This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/backend/src/routes/dashboard.ts
T
Ronny 8b7b31826a feat: stronger visual scoring differentiation + streak fix
- Exakt cards: gold glow border, gold banner, trophy emoji, larger animated badge
- Tendency: green accent (was blue), clearer differentiation from Exakt
- Falsch: muted gray, reduced opacity — clearly "lost"
- Profile tip history: solid gold/green/gray badges with distinct borders
- Streak: remove utc_date <= NOW() filter so dev-finished matches count; handle PostgreSQL boolean serialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 09:31:15 +02:00

124 lines
4.1 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { query } from '../db/client';
import { logger } from '../services/logger';
const router = Router();
router.get('/', async (req: Request, res: Response): Promise<void> => {
const userId = req.staffbaseUser?.sub;
if (!userId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
try {
// 1. Hero: next upcoming match
const heroResult = await query<any>(
`SELECT m.id, m.utc_date, m.status,
m.home_team_name, m.home_team_short, m.home_team_crest,
m.away_team_name, m.away_team_short, m.away_team_crest,
t.tip_home, t.tip_away,
EXTRACT(EPOCH FROM (m.utc_date - NOW())) / 60 AS minutes_until
FROM matches m
LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
WHERE m.status IN ('SCHEDULED', 'TIMED')
ORDER BY m.utc_date ASC
LIMIT 1`,
[userId]
);
const h = heroResult[0];
const hero = h ? {
match: {
id: h.id,
homeTeam: { name: h.home_team_name, shortName: h.home_team_short, crest: h.home_team_crest },
awayTeam: { name: h.away_team_name, shortName: h.away_team_short, crest: h.away_team_crest },
utcDate: h.utc_date,
status: h.status,
minutesUntilKickoff: Math.round(parseFloat(h.minutes_until)),
},
userTip: h.tip_home != null ? { home: h.tip_home, away: h.tip_away } : null,
tippable: parseFloat(h.minutes_until) > 5,
} : null;
// 2. Stats from leaderboard
const statsResult = await query<any>(
`SELECT rank, total_points FROM leaderboard WHERE user_id = $1`,
[userId]
);
const s = statsResult[0];
// 3. Streak: consecutive tipped matches (most recent backward)
const pastMatches = await query<{ has_tip: boolean }>(
`SELECT CASE WHEN t.id IS NOT NULL THEN true ELSE false END AS has_tip
FROM matches m
LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
WHERE m.status IN ('FINISHED', 'IN_PLAY')
ORDER BY m.utc_date DESC`,
[userId]
);
let streak = 0;
for (const m of pastMatches) {
if (m.has_tip === true || (m.has_tip as unknown) === 't' || (m.has_tip as unknown) === '1') streak++;
else break;
}
// 4. Nudges
const nudges: Array<{ type: string; text: string; matchId?: number }> = [];
const untipped = await query<{ count: string }>(
`SELECT COUNT(*) AS count FROM matches m
LEFT JOIN tips t ON t.match_id = m.id AND t.user_id = $1
WHERE m.utc_date::date = CURRENT_DATE
AND m.status IN ('SCHEDULED', 'TIMED')
AND t.id IS NULL`,
[userId]
);
const untippedCount = parseInt(untipped[0]?.count || '0');
if (untippedCount > 0) {
nudges.push({
type: 'untipped',
text: `📅 Heute noch ${untippedCount} ${untippedCount === 1 ? 'Spiel' : 'Spiele'} ohne Tipp`,
});
}
const leader = await query<{ full_name: string; total_points: string }>(
`SELECT full_name, total_points FROM leaderboard ORDER BY rank ASC LIMIT 1`
);
if (leader[0]) {
nudges.push({ type: 'leader', text: `🏆 ${leader[0].full_name} führt mit ${leader[0].total_points} Punkten` });
}
const latest = await query<any>(
`SELECT m.home_team_short, m.away_team_short, m.score_home, m.score_away, t.points, m.id AS match_id
FROM tips t JOIN matches m ON m.id = t.match_id
WHERE t.user_id = $1 AND t.points IS NOT NULL
ORDER BY m.utc_date DESC LIMIT 1`,
[userId]
);
if (latest[0]) {
const r = latest[0];
nudges.push({
type: 'result',
text: `🎯 Letzte Auswertung: ${r.points} Punkte für ${r.home_team_short} ${r.score_home}:${r.score_away} ${r.away_team_short}`,
matchId: r.match_id,
});
}
res.json({
hero,
stats: {
rank: s ? parseInt(s.rank) : null,
totalPoints: s ? parseInt(s.total_points) : 0,
streak,
},
nudges,
});
} catch (error) {
logger.error('Dashboard failed', { error });
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;