Diverse änderungen am Push system
This commit is contained in:
@@ -89,7 +89,17 @@ class AchievementSystem {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`📋 ${result.rows.length} Achievements für Spieler ${playerId} geladen`);
|
// Count unique achievements and duplicates
|
||||||
|
const uniqueAchievements = new Set(result.rows.map(row => row.achievement_id));
|
||||||
|
const totalCount = result.rows.length;
|
||||||
|
const uniqueCount = uniqueAchievements.size;
|
||||||
|
const duplicateCount = totalCount - uniqueCount;
|
||||||
|
|
||||||
|
if (duplicateCount > 0) {
|
||||||
|
console.log(`📋 ${totalCount} Achievements für Spieler ${playerId} geladen (${uniqueCount} eindeutige, ${duplicateCount} doppelt)`);
|
||||||
|
} else {
|
||||||
|
console.log(`📋 ${totalCount} Achievements für Spieler ${playerId} geladen`);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Fehler beim Laden der Spieler-Achievements für ${playerId}:`, error);
|
console.error(`❌ Fehler beim Laden der Spieler-Achievements für ${playerId}:`, error);
|
||||||
@@ -535,106 +545,138 @@ class AchievementSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft Tageskönig Achievement
|
* Prüft Tageskönig Achievement pro Standort
|
||||||
*/
|
*/
|
||||||
async checkDailyBest(playerId, currentDate, newAchievements) {
|
async checkDailyBest(playerId, currentDate, newAchievements) {
|
||||||
const achievement = Array.from(this.achievements.values())
|
const achievement = Array.from(this.achievements.values())
|
||||||
.find(a => a.category === 'best_time' && a.condition_type === 'daily_best');
|
.find(a => a.category === 'time' && a.condition_type === 'best_time_daily_location');
|
||||||
|
|
||||||
if (!achievement) return;
|
if (!achievement) return;
|
||||||
|
|
||||||
// Prüfe ob das Achievement heute bereits vergeben wurde
|
// Hole alle Standorte, an denen der Spieler heute gespielt hat
|
||||||
const alreadyEarnedToday = await pool.query(`
|
const locationsResult = await pool.query(`
|
||||||
SELECT COUNT(*) as count
|
SELECT DISTINCT t.location_id, l.name as location_name
|
||||||
FROM player_achievements pa
|
|
||||||
WHERE pa.player_id = $1
|
|
||||||
AND pa.achievement_id = $2
|
|
||||||
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
|
||||||
`, [playerId, achievement.id]);
|
|
||||||
|
|
||||||
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) return;
|
|
||||||
|
|
||||||
// Hole beste Zeit des Spielers heute
|
|
||||||
const playerResult = await pool.query(`
|
|
||||||
SELECT MIN(recorded_time) as best_time
|
|
||||||
FROM times t
|
FROM times t
|
||||||
|
INNER JOIN locations l ON t.location_id = l.id
|
||||||
WHERE t.player_id = $1
|
WHERE t.player_id = $1
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $2
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $2
|
||||||
`, [playerId, currentDate]);
|
`, [playerId, currentDate]);
|
||||||
|
|
||||||
// Hole beste Zeit des Tages
|
for (const location of locationsResult.rows) {
|
||||||
const dailyResult = await pool.query(`
|
// Prüfe ob das Achievement heute bereits für diesen Standort vergeben wurde
|
||||||
SELECT MIN(recorded_time) as best_time
|
const alreadyEarnedToday = await pool.query(`
|
||||||
FROM times t
|
SELECT COUNT(*) as count
|
||||||
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1
|
FROM player_achievements pa
|
||||||
`, [currentDate]);
|
WHERE pa.player_id = $1
|
||||||
|
AND pa.achievement_id = $2
|
||||||
|
AND pa.location_id = $3
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
|
||||||
|
`, [playerId, achievement.id, location.location_id]);
|
||||||
|
|
||||||
|
if (parseInt(alreadyEarnedToday.rows[0].count) > 0) continue;
|
||||||
|
|
||||||
const playerBest = playerResult.rows[0].best_time;
|
// Hole beste Zeit des Spielers heute an diesem Standort
|
||||||
const dailyBest = dailyResult.rows[0].best_time;
|
const playerResult = await pool.query(`
|
||||||
|
SELECT MIN(recorded_time) as best_time
|
||||||
|
FROM times t
|
||||||
|
WHERE t.player_id = $1
|
||||||
|
AND t.location_id = $2
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $3
|
||||||
|
`, [playerId, location.location_id, currentDate]);
|
||||||
|
|
||||||
if (playerBest && dailyBest && playerBest === dailyBest) {
|
// Hole beste Zeit des Tages an diesem Standort
|
||||||
await this.awardAchievement(playerId, achievement, 1, newAchievements);
|
const dailyResult = await pool.query(`
|
||||||
|
SELECT MIN(recorded_time) as best_time
|
||||||
|
FROM times t
|
||||||
|
WHERE t.location_id = $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $2
|
||||||
|
`, [location.location_id, currentDate]);
|
||||||
|
|
||||||
|
const playerBest = playerResult.rows[0].best_time;
|
||||||
|
const dailyBest = dailyResult.rows[0].best_time;
|
||||||
|
|
||||||
|
if (playerBest && dailyBest && playerBest === dailyBest) {
|
||||||
|
await this.awardAchievement(playerId, achievement, 1, newAchievements, location.location_id);
|
||||||
|
console.log(`🏆 Tageskönig Achievement vergeben für Standort: ${location.location_name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft Wochenchampion Achievement
|
* Prüft Wochenchampion Achievement pro Standort
|
||||||
*/
|
*/
|
||||||
async checkWeeklyBest(playerId, currentDate, newAchievements) {
|
async checkWeeklyBest(playerId, currentDate, newAchievements) {
|
||||||
const achievement = Array.from(this.achievements.values())
|
const achievement = Array.from(this.achievements.values())
|
||||||
.find(a => a.category === 'best_time' && a.condition_type === 'weekly_best');
|
.find(a => a.category === 'time' && a.condition_type === 'best_time_weekly_location');
|
||||||
|
|
||||||
if (!achievement) return;
|
if (!achievement) return;
|
||||||
|
|
||||||
// Prüfe ob das Achievement diese Woche bereits vergeben wurde
|
// Berechne Woche
|
||||||
const currentDateObj = new Date(currentDate);
|
const currentDateObj = new Date(currentDate);
|
||||||
const dayOfWeek = currentDateObj.getDay();
|
const dayOfWeek = currentDateObj.getDay();
|
||||||
const weekStart = new Date(currentDateObj);
|
const weekStart = new Date(currentDateObj);
|
||||||
weekStart.setDate(currentDateObj.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
|
weekStart.setDate(currentDateObj.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
|
||||||
const weekStartStr = weekStart.toISOString().split('T')[0];
|
const weekStartStr = weekStart.toISOString().split('T')[0];
|
||||||
|
|
||||||
const alreadyEarnedThisWeek = await pool.query(`
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM player_achievements pa
|
|
||||||
WHERE pa.player_id = $1
|
|
||||||
AND pa.achievement_id = $2
|
|
||||||
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $3
|
|
||||||
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $4
|
|
||||||
`, [playerId, achievement.id, weekStartStr, currentDate]);
|
|
||||||
|
|
||||||
if (parseInt(alreadyEarnedThisWeek.rows[0].count) > 0) return;
|
|
||||||
|
|
||||||
// Hole beste Zeit des Spielers diese Woche
|
// Hole alle Standorte, an denen der Spieler diese Woche gespielt hat
|
||||||
const playerResult = await pool.query(`
|
const locationsResult = await pool.query(`
|
||||||
SELECT MIN(recorded_time) as best_time
|
SELECT DISTINCT t.location_id, l.name as location_name
|
||||||
FROM times t
|
FROM times t
|
||||||
|
INNER JOIN locations l ON t.location_id = l.id
|
||||||
WHERE t.player_id = $1
|
WHERE t.player_id = $1
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
|
||||||
`, [playerId, weekStartStr, currentDate]);
|
`, [playerId, weekStartStr, currentDate]);
|
||||||
|
|
||||||
// Hole beste Zeit der Woche
|
for (const location of locationsResult.rows) {
|
||||||
const weeklyResult = await pool.query(`
|
// Prüfe ob das Achievement diese Woche bereits für diesen Standort vergeben wurde
|
||||||
SELECT MIN(recorded_time) as best_time
|
const alreadyEarnedThisWeek = await pool.query(`
|
||||||
FROM times t
|
SELECT COUNT(*) as count
|
||||||
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
|
FROM player_achievements pa
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
|
WHERE pa.player_id = $1
|
||||||
`, [weekStartStr, currentDate]);
|
AND pa.achievement_id = $2
|
||||||
|
AND pa.location_id = $3
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $4
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $5
|
||||||
|
`, [playerId, achievement.id, location.location_id, weekStartStr, currentDate]);
|
||||||
|
|
||||||
|
if (parseInt(alreadyEarnedThisWeek.rows[0].count) > 0) continue;
|
||||||
|
|
||||||
const playerBest = playerResult.rows[0].best_time;
|
// Hole beste Zeit des Spielers diese Woche an diesem Standort
|
||||||
const weeklyBest = weeklyResult.rows[0].best_time;
|
const playerResult = await pool.query(`
|
||||||
|
SELECT MIN(recorded_time) as best_time
|
||||||
|
FROM times t
|
||||||
|
WHERE t.player_id = $1
|
||||||
|
AND t.location_id = $2
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $3
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $4
|
||||||
|
`, [playerId, location.location_id, weekStartStr, currentDate]);
|
||||||
|
|
||||||
if (playerBest && weeklyBest && playerBest === weeklyBest) {
|
// Hole beste Zeit der Woche an diesem Standort
|
||||||
await this.awardAchievement(playerId, achievement, 1, newAchievements);
|
const weeklyResult = await pool.query(`
|
||||||
|
SELECT MIN(recorded_time) as best_time
|
||||||
|
FROM times t
|
||||||
|
WHERE t.location_id = $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
|
||||||
|
`, [location.location_id, weekStartStr, currentDate]);
|
||||||
|
|
||||||
|
const playerBest = playerResult.rows[0].best_time;
|
||||||
|
const weeklyBest = weeklyResult.rows[0].best_time;
|
||||||
|
|
||||||
|
if (playerBest && weeklyBest && playerBest === weeklyBest) {
|
||||||
|
await this.awardAchievement(playerId, achievement, 1, newAchievements, location.location_id);
|
||||||
|
console.log(`🏆 Wochenchampion Achievement vergeben für Standort: ${location.location_name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft Monatsmeister Achievement
|
* Prüft Monatsmeister Achievement pro Standort
|
||||||
*/
|
*/
|
||||||
async checkMonthlyBest(playerId, currentDate, newAchievements) {
|
async checkMonthlyBest(playerId, currentDate, newAchievements) {
|
||||||
const achievement = Array.from(this.achievements.values())
|
const achievement = Array.from(this.achievements.values())
|
||||||
.find(a => a.category === 'best_time' && a.condition_type === 'monthly_best');
|
.find(a => a.category === 'time' && a.condition_type === 'best_time_monthly_location');
|
||||||
|
|
||||||
if (!achievement) return;
|
if (!achievement) return;
|
||||||
|
|
||||||
@@ -642,41 +684,57 @@ class AchievementSystem {
|
|||||||
const currentDateObj = new Date(currentDate);
|
const currentDateObj = new Date(currentDate);
|
||||||
const monthStart = new Date(currentDateObj.getFullYear(), currentDateObj.getMonth(), 1);
|
const monthStart = new Date(currentDateObj.getFullYear(), currentDateObj.getMonth(), 1);
|
||||||
const monthStartStr = monthStart.toISOString().split('T')[0];
|
const monthStartStr = monthStart.toISOString().split('T')[0];
|
||||||
|
|
||||||
// Prüfe ob das Achievement diesen Monat bereits vergeben wurde
|
|
||||||
const alreadyEarnedThisMonth = await pool.query(`
|
|
||||||
SELECT COUNT(*) as count
|
|
||||||
FROM player_achievements pa
|
|
||||||
WHERE pa.player_id = $1
|
|
||||||
AND pa.achievement_id = $2
|
|
||||||
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $3
|
|
||||||
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $4
|
|
||||||
`, [playerId, achievement.id, monthStartStr, currentDate]);
|
|
||||||
|
|
||||||
if (parseInt(alreadyEarnedThisMonth.rows[0].count) > 0) return;
|
|
||||||
|
|
||||||
// Hole beste Zeit des Spielers diesen Monat
|
// Hole alle Standorte, an denen der Spieler diesen Monat gespielt hat
|
||||||
const playerResult = await pool.query(`
|
const locationsResult = await pool.query(`
|
||||||
SELECT MIN(recorded_time) as best_time
|
SELECT DISTINCT t.location_id, l.name as location_name
|
||||||
FROM times t
|
FROM times t
|
||||||
|
INNER JOIN locations l ON t.location_id = l.id
|
||||||
WHERE t.player_id = $1
|
WHERE t.player_id = $1
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
|
||||||
`, [playerId, monthStartStr, currentDate]);
|
`, [playerId, monthStartStr, currentDate]);
|
||||||
|
|
||||||
// Hole beste Zeit des Monats
|
for (const location of locationsResult.rows) {
|
||||||
const monthlyResult = await pool.query(`
|
// Prüfe ob das Achievement diesen Monat bereits für diesen Standort vergeben wurde
|
||||||
SELECT MIN(recorded_time) as best_time
|
const alreadyEarnedThisMonth = await pool.query(`
|
||||||
FROM times t
|
SELECT COUNT(*) as count
|
||||||
WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1
|
FROM player_achievements pa
|
||||||
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2
|
WHERE pa.player_id = $1
|
||||||
`, [monthStartStr, currentDate]);
|
AND pa.achievement_id = $2
|
||||||
|
AND pa.location_id = $3
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') >= $4
|
||||||
|
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') <= $5
|
||||||
|
`, [playerId, achievement.id, location.location_id, monthStartStr, currentDate]);
|
||||||
|
|
||||||
|
if (parseInt(alreadyEarnedThisMonth.rows[0].count) > 0) continue;
|
||||||
|
|
||||||
const playerBest = playerResult.rows[0].best_time;
|
// Hole beste Zeit des Spielers diesen Monat an diesem Standort
|
||||||
const monthlyBest = monthlyResult.rows[0].best_time;
|
const playerResult = await pool.query(`
|
||||||
|
SELECT MIN(recorded_time) as best_time
|
||||||
|
FROM times t
|
||||||
|
WHERE t.player_id = $1
|
||||||
|
AND t.location_id = $2
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $3
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $4
|
||||||
|
`, [playerId, location.location_id, monthStartStr, currentDate]);
|
||||||
|
|
||||||
if (playerBest && monthlyBest && playerBest === monthlyBest) {
|
// Hole beste Zeit des Monats an diesem Standort
|
||||||
await this.awardAchievement(playerId, achievement, 1, newAchievements);
|
const monthlyResult = await pool.query(`
|
||||||
|
SELECT MIN(recorded_time) as best_time
|
||||||
|
FROM times t
|
||||||
|
WHERE t.location_id = $1
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2
|
||||||
|
AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3
|
||||||
|
`, [location.location_id, monthStartStr, currentDate]);
|
||||||
|
|
||||||
|
const playerBest = playerResult.rows[0].best_time;
|
||||||
|
const monthlyBest = monthlyResult.rows[0].best_time;
|
||||||
|
|
||||||
|
if (playerBest && monthlyBest && playerBest === monthlyBest) {
|
||||||
|
await this.awardAchievement(playerId, achievement, 1, newAchievements, location.location_id);
|
||||||
|
console.log(`🏆 Monatsmeister Achievement vergeben für Standort: ${location.location_name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,12 +742,12 @@ class AchievementSystem {
|
|||||||
* Vergibt ein Achievement an einen Spieler
|
* Vergibt ein Achievement an einen Spieler
|
||||||
* Erstellt immer einen neuen Eintrag (keine Updates mehr)
|
* Erstellt immer einen neuen Eintrag (keine Updates mehr)
|
||||||
*/
|
*/
|
||||||
async awardAchievement(playerId, achievement, progress, newAchievements) {
|
async awardAchievement(playerId, achievement, progress, newAchievements, locationId = null) {
|
||||||
try {
|
try {
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO player_achievements (player_id, achievement_id, progress, is_completed, earned_at)
|
INSERT INTO player_achievements (player_id, achievement_id, progress, is_completed, earned_at, location_id)
|
||||||
VALUES ($1, $2, $3, true, NOW())
|
VALUES ($1, $2, $3, true, NOW(), $4)
|
||||||
`, [playerId, achievement.id, progress]);
|
`, [playerId, achievement.id, progress, locationId]);
|
||||||
|
|
||||||
newAchievements.push({
|
newAchievements.push({
|
||||||
id: achievement.id,
|
id: achievement.id,
|
||||||
@@ -697,10 +755,12 @@ class AchievementSystem {
|
|||||||
description: achievement.description,
|
description: achievement.description,
|
||||||
icon: achievement.icon,
|
icon: achievement.icon,
|
||||||
points: achievement.points,
|
points: achievement.points,
|
||||||
progress: progress
|
progress: progress,
|
||||||
|
locationId: locationId
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`🏆 Achievement vergeben: ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`);
|
const locationText = locationId ? ` (Standort: ${locationId})` : '';
|
||||||
|
console.log(`🏆 Achievement vergeben: ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)${locationText}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Fehler beim Vergeben des Achievements ${achievement.name}:`, error);
|
console.error(`❌ Fehler beim Vergeben des Achievements ${achievement.name}:`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,52 @@ body {
|
|||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Push notification button */
|
||||||
|
.btn-push {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-push:hover {
|
||||||
|
background: #059669;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-push.active {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-push.active:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-push::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #fbbf24;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-push.active::after {
|
||||||
|
opacity: 1;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.2); opacity: 0.7; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Logout button */
|
/* Logout button */
|
||||||
.btn-logout {
|
.btn-logout {
|
||||||
background: #dc2626;
|
background: #dc2626;
|
||||||
|
|||||||
@@ -29,42 +29,259 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request notification permission on page load
|
// Don't automatically request notification permission
|
||||||
if ('Notification' in window) {
|
// User must click the button to enable push notifications
|
||||||
if (Notification.permission === 'default') {
|
|
||||||
Notification.requestPermission().then(function(permission) {
|
|
||||||
if (permission === 'granted') {
|
|
||||||
console.log('✅ Notification permission granted');
|
|
||||||
// Subscribe to push notifications
|
|
||||||
subscribeToPush();
|
|
||||||
} else {
|
|
||||||
console.log('❌ Notification permission denied');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Convert VAPID key from base64url to Uint8Array
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ArrayBuffer to Base64 string
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push notification state
|
||||||
|
let pushSubscription = null;
|
||||||
|
let pushEnabled = false;
|
||||||
|
|
||||||
// Subscribe to push notifications
|
// Subscribe to push notifications
|
||||||
async function subscribeToPush() {
|
async function subscribeToPush() {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔔 Starting push subscription...');
|
||||||
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
|
||||||
|
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
|
||||||
|
|
||||||
const subscription = await registration.pushManager.subscribe({
|
const subscription = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds'
|
applicationServerKey: applicationServerKey
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send subscription to server
|
pushSubscription = subscription;
|
||||||
await fetch('/api/v1/public/subscribe', {
|
|
||||||
|
// Generate or get player ID
|
||||||
|
let playerId = window.currentPlayerId;
|
||||||
|
if (!playerId) {
|
||||||
|
// Generate a UUID for the player
|
||||||
|
playerId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
console.log(`📱 Generated player ID: ${playerId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`📱 Using existing player ID: ${playerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ArrayBuffer keys to Base64 strings
|
||||||
|
const p256dhKey = subscription.getKey('p256dh');
|
||||||
|
const authKey = subscription.getKey('auth');
|
||||||
|
|
||||||
|
// Convert ArrayBuffer to Base64 URL-safe string
|
||||||
|
const p256dhString = arrayBufferToBase64(p256dhKey);
|
||||||
|
const authString = arrayBufferToBase64(authKey);
|
||||||
|
|
||||||
|
console.log('📱 Converted keys to Base64 strings');
|
||||||
|
console.log('📱 p256dh length:', p256dhString.length);
|
||||||
|
console.log('📱 auth length:', authString.length);
|
||||||
|
|
||||||
|
// Send subscription to server with player ID
|
||||||
|
const response = await fetch('/api/v1/public/subscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(subscription)
|
body: JSON.stringify({
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: p256dhString,
|
||||||
|
auth: authString
|
||||||
|
},
|
||||||
|
playerId: playerId
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('✅ Push subscription successful');
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
pushEnabled = true;
|
||||||
|
updatePushButton();
|
||||||
|
console.log('✅ Push subscription successful');
|
||||||
|
|
||||||
|
// Store player ID for notifications
|
||||||
|
if (result.playerId) {
|
||||||
|
localStorage.setItem('pushPlayerId', result.playerId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Push subscription failed:', error);
|
console.error('❌ Push subscription failed:', error);
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from push notifications
|
||||||
|
async function unsubscribeFromPush() {
|
||||||
|
try {
|
||||||
|
console.log('🔕 Unsubscribing from push notifications...');
|
||||||
|
|
||||||
|
// Get player ID from localStorage
|
||||||
|
const playerId = localStorage.getItem('pushPlayerId');
|
||||||
|
|
||||||
|
if (pushSubscription) {
|
||||||
|
await pushSubscription.unsubscribe();
|
||||||
|
pushSubscription = null;
|
||||||
|
console.log('✅ Local push subscription removed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify server to remove from database
|
||||||
|
if (playerId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/public/unsubscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ playerId: playerId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
console.log('✅ Server notified - subscription removed from database');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Server notification failed:', result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Failed to notify server:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear stored player ID
|
||||||
|
localStorage.removeItem('pushPlayerId');
|
||||||
|
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
|
console.log('🔕 Push notifications disabled');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Push unsubscribe failed:', error);
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle push notifications
|
||||||
|
async function togglePushNotifications() {
|
||||||
|
if (pushEnabled) {
|
||||||
|
await unsubscribeFromPush();
|
||||||
|
} else {
|
||||||
|
// Check notification permission first
|
||||||
|
if (Notification.permission === 'denied') {
|
||||||
|
alert('Push-Benachrichtigungen sind blockiert. Bitte erlaube sie in den Browser-Einstellungen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission === 'default') {
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
if (permission !== 'granted') {
|
||||||
|
alert('Push-Benachrichtigungen wurden nicht erlaubt.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await subscribeToPush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update push button appearance
|
||||||
|
function updatePushButton() {
|
||||||
|
const button = document.getElementById('pushButton');
|
||||||
|
if (!button) {
|
||||||
|
console.log('❌ Push button not found in DOM');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔔 Updating push button - Status: ${pushEnabled ? 'ENABLED' : 'DISABLED'}`);
|
||||||
|
|
||||||
|
if (pushEnabled) {
|
||||||
|
button.classList.add('active');
|
||||||
|
button.setAttribute('data-de', '🔕 Push deaktivieren');
|
||||||
|
button.setAttribute('data-en', '🔕 Disable Push');
|
||||||
|
button.textContent = '🔕 Push deaktivieren';
|
||||||
|
console.log('✅ Button updated to: Push deaktivieren (RED)');
|
||||||
|
} else {
|
||||||
|
button.classList.remove('active');
|
||||||
|
button.setAttribute('data-de', '🔔 Push aktivieren');
|
||||||
|
button.setAttribute('data-en', '🔔 Enable Push');
|
||||||
|
button.textContent = '🔔 Push aktivieren';
|
||||||
|
console.log('✅ Button updated to: Push aktivieren (GREEN)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existing push subscription on page load
|
||||||
|
async function checkPushStatus() {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Checking push status...');
|
||||||
|
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
console.log('❌ Service Worker not supported');
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('PushManager' in window)) {
|
||||||
|
console.log('❌ Push Manager not supported');
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
console.log('✅ Service Worker ready');
|
||||||
|
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
console.log('📱 Current subscription:', subscription ? 'EXISTS' : 'NONE');
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
pushSubscription = subscription;
|
||||||
|
pushEnabled = true;
|
||||||
|
updatePushButton();
|
||||||
|
console.log('✅ Existing push subscription found and activated');
|
||||||
|
|
||||||
|
// Also check if we have a stored player ID
|
||||||
|
const storedPlayerId = localStorage.getItem('pushPlayerId');
|
||||||
|
if (storedPlayerId) {
|
||||||
|
console.log(`📱 Push subscription linked to player: ${storedPlayerId}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
|
console.log('ℹ️ No existing push subscription found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error checking push status:', error);
|
||||||
|
pushEnabled = false;
|
||||||
|
updatePushButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -84,6 +301,7 @@
|
|||||||
<div class="user-avatar" id="userAvatar">U</div>
|
<div class="user-avatar" id="userAvatar">U</div>
|
||||||
<span id="userEmail">user@example.com</span>
|
<span id="userEmail">user@example.com</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-push" id="pushButton" onclick="togglePushNotifications()" data-de="🔔 Push aktivieren" data-en="🔔 Enable Push">🔔 Push aktivieren</button>
|
||||||
<a href="/" class="btn btn-primary" data-de="Zurück zu Zeiten" data-en="Back to Times">Back to Times</a>
|
<a href="/" class="btn btn-primary" data-de="Zurück zu Zeiten" data-en="Back to Times">Back to Times</a>
|
||||||
<button class="btn btn-logout" onclick="logout()" data-de="Logout" data-en="Logout">Logout</button>
|
<button class="btn btn-logout" onclick="logout()" data-de="Logout" data-en="Logout">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -324,6 +324,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
loadLanguagePreference();
|
loadLanguagePreference();
|
||||||
changeLanguage(); // Apply saved language
|
changeLanguage(); // Apply saved language
|
||||||
initDashboard();
|
initDashboard();
|
||||||
|
|
||||||
|
// Push notifications are now handled manually via the button
|
||||||
|
// No automatic subscription on page load
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modal functions
|
// Modal functions
|
||||||
@@ -1739,6 +1742,25 @@ function initializeAchievements(playerId) {
|
|||||||
loadPlayerAchievements();
|
loadPlayerAchievements();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert VAPID key from base64url to Uint8Array
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push notifications are now handled in dashboard.html
|
||||||
|
// This function has been moved to the HTML file for better integration
|
||||||
|
|
||||||
// Web Notification Functions
|
// Web Notification Functions
|
||||||
function showWebNotification(title, message, icon = '🏆') {
|
function showWebNotification(title, message, icon = '🏆') {
|
||||||
if ('Notification' in window && Notification.permission === 'granted') {
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
@@ -1763,9 +1785,23 @@ function showWebNotification(title, message, icon = '🏆') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track which notifications have been sent today
|
||||||
|
let notificationsSentToday = {
|
||||||
|
daily: false,
|
||||||
|
weekly: false,
|
||||||
|
monthly: false
|
||||||
|
};
|
||||||
|
|
||||||
// Check for best time achievements and show notifications
|
// Check for best time achievements and show notifications
|
||||||
async function checkBestTimeNotifications() {
|
async function checkBestTimeNotifications() {
|
||||||
try {
|
try {
|
||||||
|
// Check if push notifications are enabled
|
||||||
|
const pushPlayerId = localStorage.getItem('pushPlayerId');
|
||||||
|
if (!pushPlayerId) {
|
||||||
|
console.log('🔕 Push notifications disabled, skipping best time check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/v1/public/best-times');
|
const response = await fetch('/api/v1/public/best-times');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -1774,28 +1810,95 @@ async function checkBestTimeNotifications() {
|
|||||||
|
|
||||||
// Check if current player has best times
|
// Check if current player has best times
|
||||||
if (currentPlayerId) {
|
if (currentPlayerId) {
|
||||||
if (daily && daily.player_id === currentPlayerId) {
|
const now = new Date();
|
||||||
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
|
const isEvening = now.getHours() >= 19;
|
||||||
const message = currentLanguage === 'de' ?
|
|
||||||
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` :
|
// Check daily best time (only in the evening, only once per day)
|
||||||
`Congratulations! You achieved the best time of the day with ${daily.best_time}!`;
|
if (daily && daily.player_id === currentPlayerId && isEvening) {
|
||||||
showWebNotification(title, message, '👑');
|
// Check if notification was already sent today
|
||||||
|
const dailyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/daily_best`);
|
||||||
|
const dailyResult = await dailyCheck.json();
|
||||||
|
|
||||||
|
if (!dailyResult.wasSent) {
|
||||||
|
const title = currentLanguage === 'de' ? '🏆 Tageskönig!' : '🏆 Daily King!';
|
||||||
|
const message = currentLanguage === 'de' ?
|
||||||
|
`Glückwunsch! Du hast die beste Zeit des Tages mit ${daily.best_time} erreicht!` :
|
||||||
|
`Congratulations! You achieved the best time of the day with ${daily.best_time}!`;
|
||||||
|
showWebNotification(title, message, '👑');
|
||||||
|
|
||||||
|
// Mark as sent in database
|
||||||
|
await fetch('/api/v1/public/notification-sent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
playerId: currentPlayerId,
|
||||||
|
notificationType: 'daily_best'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log('🏆 Daily best notification sent');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check weekly best time (only on Sunday evening, only once per week)
|
||||||
if (weekly && weekly.player_id === currentPlayerId) {
|
if (weekly && weekly.player_id === currentPlayerId) {
|
||||||
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
|
const isSunday = now.getDay() === 0; // 0 = Sunday
|
||||||
const message = currentLanguage === 'de' ?
|
|
||||||
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` :
|
if (isSunday && isEvening) {
|
||||||
`Fantastic! You are the weekly champion with ${weekly.best_time}!`;
|
// Check if notification was already sent this week
|
||||||
showWebNotification(title, message, '🏆');
|
const weeklyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/weekly_best`);
|
||||||
|
const weeklyResult = await weeklyCheck.json();
|
||||||
|
|
||||||
|
if (!weeklyResult.wasSent) {
|
||||||
|
const title = currentLanguage === 'de' ? '🏆 Wochenchampion!' : '🏆 Weekly Champion!';
|
||||||
|
const message = currentLanguage === 'de' ?
|
||||||
|
`Fantastisch! Du bist der Wochenchampion mit ${weekly.best_time}!` :
|
||||||
|
`Fantastic! You are the weekly champion with ${weekly.best_time}!`;
|
||||||
|
showWebNotification(title, message, '🏆');
|
||||||
|
|
||||||
|
// Mark as sent in database
|
||||||
|
await fetch('/api/v1/public/notification-sent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
playerId: currentPlayerId,
|
||||||
|
notificationType: 'weekly_best'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log('🏆 Weekly best notification sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check monthly best time (only on last evening of month at 19:00, only once per month)
|
||||||
if (monthly && monthly.player_id === currentPlayerId) {
|
if (monthly && monthly.player_id === currentPlayerId) {
|
||||||
const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!';
|
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
const message = currentLanguage === 'de' ?
|
const isLastDayOfMonth = now.getDate() === lastDayOfMonth.getDate();
|
||||||
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!` :
|
|
||||||
`Incredible! You are the monthly master with ${monthly.best_time}!`;
|
// Only show monthly notification on last day of month at 19:00 or later
|
||||||
showWebNotification(title, message, '🥇');
|
if (isLastDayOfMonth && isEvening) {
|
||||||
|
// Check if notification was already sent this month
|
||||||
|
const monthlyCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/monthly_best`);
|
||||||
|
const monthlyResult = await monthlyCheck.json();
|
||||||
|
|
||||||
|
if (!monthlyResult.wasSent) {
|
||||||
|
const title = currentLanguage === 'de' ? '🏆 Monatsmeister!' : '🏆 Monthly Master!';
|
||||||
|
const message = currentLanguage === 'de' ?
|
||||||
|
`Unglaublich! Du bist der Monatsmeister mit ${monthly.best_time}!` :
|
||||||
|
`Incredible! You are the monthly master with ${monthly.best_time}!`;
|
||||||
|
showWebNotification(title, message, '🥇');
|
||||||
|
|
||||||
|
// Mark as sent in database
|
||||||
|
await fetch('/api/v1/public/notification-sent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
playerId: currentPlayerId,
|
||||||
|
notificationType: 'monthly_best'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log('🏆 Monthly best notification sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1809,6 +1912,13 @@ async function checkAchievementNotifications() {
|
|||||||
try {
|
try {
|
||||||
if (!currentPlayerId) return;
|
if (!currentPlayerId) return;
|
||||||
|
|
||||||
|
// Check if push notifications are enabled
|
||||||
|
const pushPlayerId = localStorage.getItem('pushPlayerId');
|
||||||
|
if (!pushPlayerId) {
|
||||||
|
console.log('🔕 Push notifications disabled, skipping achievement check');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
const response = await fetch(`/api/achievements/player/${currentPlayerId}?t=${Date.now()}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
@@ -1821,14 +1931,33 @@ async function checkAchievementNotifications() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (newAchievements.length > 0) {
|
if (newAchievements.length > 0) {
|
||||||
newAchievements.forEach(achievement => {
|
for (const achievement of newAchievements) {
|
||||||
const translatedAchievement = translateAchievement(achievement);
|
// Check if notification was already sent for this achievement
|
||||||
showWebNotification(
|
const achievementCheck = await fetch(`/api/v1/public/notification-sent/${currentPlayerId}/achievement?achievementId=${achievement.achievement_id}&locationId=${achievement.location_id || ''}`);
|
||||||
`🏆 ${translatedAchievement.name}`,
|
const achievementResult = await achievementCheck.json();
|
||||||
translatedAchievement.description,
|
|
||||||
achievement.icon || '🏆'
|
if (!achievementResult.wasSent) {
|
||||||
);
|
const translatedAchievement = translateAchievement(achievement);
|
||||||
});
|
showWebNotification(
|
||||||
|
`🏆 ${translatedAchievement.name}`,
|
||||||
|
translatedAchievement.description,
|
||||||
|
achievement.icon || '🏆'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark as sent in database
|
||||||
|
await fetch('/api/v1/public/notification-sent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
playerId: currentPlayerId,
|
||||||
|
notificationType: 'achievement',
|
||||||
|
achievementId: achievement.achievement_id,
|
||||||
|
locationId: achievement.location_id || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log(`🏆 Achievement notification sent: ${achievement.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1967,4 +2096,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check push notification status on dashboard load
|
||||||
|
if (typeof checkPushStatus === 'function') {
|
||||||
|
checkPushStatus();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
53
public/sw.js
53
public/sw.js
@@ -2,22 +2,65 @@
|
|||||||
const CACHE_NAME = 'ninjacross-v1';
|
const CACHE_NAME = 'ninjacross-v1';
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/test-push.html',
|
||||||
'/css/leaderboard.css',
|
'/sw.js'
|
||||||
'/js/leaderboard.js',
|
|
||||||
'/pictures/favicon.ico'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Install event
|
// Install event
|
||||||
self.addEventListener('install', function(event) {
|
self.addEventListener('install', function(event) {
|
||||||
|
console.log('Service Worker installing...');
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME)
|
caches.open(CACHE_NAME)
|
||||||
.then(function(cache) {
|
.then(function(cache) {
|
||||||
return cache.addAll(urlsToCache);
|
// Add files one by one to handle failures gracefully
|
||||||
|
return Promise.allSettled(
|
||||||
|
urlsToCache.map(url =>
|
||||||
|
cache.add(url).catch(err => {
|
||||||
|
console.warn(`Failed to cache ${url}:`, err);
|
||||||
|
return null; // Continue with other files
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Service Worker installation completed');
|
||||||
|
// Skip waiting to activate immediately
|
||||||
|
return self.skipWaiting();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Service Worker installation failed:', err);
|
||||||
|
// Still try to skip waiting
|
||||||
|
return self.skipWaiting();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Activate event
|
||||||
|
self.addEventListener('activate', function(event) {
|
||||||
|
console.log('Service Worker activating...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(function(cacheNames) {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map(function(cacheName) {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}).then(() => {
|
||||||
|
// Take control of all clients immediately
|
||||||
|
return self.clients.claim();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for skip waiting message
|
||||||
|
self.addEventListener('message', function(event) {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch event
|
// Fetch event
|
||||||
self.addEventListener('fetch', function(event) {
|
self.addEventListener('fetch', function(event) {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
<input type="text" id="testMessage" placeholder="Test-Nachricht" value="Das ist eine Test-Push-Notification!">
|
<input type="text" id="testMessage" placeholder="Test-Nachricht" value="Das ist eine Test-Push-Notification!">
|
||||||
<button onclick="sendTestPush()">Test-Push senden</button>
|
<button onclick="sendTestPush()">Test-Push senden</button>
|
||||||
<button onclick="sendTestWebNotification()">Web-Notification senden</button>
|
<button onclick="sendTestWebNotification()">Web-Notification senden</button>
|
||||||
|
<button onclick="sendWindowsNotification()">Windows-Notification senden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -120,6 +121,22 @@
|
|||||||
<script>
|
<script>
|
||||||
let currentSubscription = null;
|
let currentSubscription = null;
|
||||||
|
|
||||||
|
// Convert VAPID key from base64url to Uint8Array
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
function log(message, type = 'info') {
|
function log(message, type = 'info') {
|
||||||
const logDiv = document.getElementById('log');
|
const logDiv = document.getElementById('log');
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
@@ -155,14 +172,19 @@
|
|||||||
async function registerServiceWorker() {
|
async function registerServiceWorker() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
try {
|
try {
|
||||||
|
log('Service Worker wird registriert...', 'info');
|
||||||
const registration = await navigator.serviceWorker.register('/sw.js');
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||||
log('Service Worker erfolgreich registriert', 'success');
|
log('Service Worker erfolgreich registriert', 'success');
|
||||||
log(`SW Scope: ${registration.scope}`);
|
log(`SW Scope: ${registration.scope}`);
|
||||||
|
updateStatus('Service Worker registriert', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
|
log(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
|
||||||
|
log(`Error Details: ${JSON.stringify(error)}`, 'error');
|
||||||
|
updateStatus(`Service Worker Registrierung fehlgeschlagen: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log('Service Worker nicht unterstützt', 'error');
|
log('Service Worker nicht unterstützt', 'error');
|
||||||
|
updateStatus('Service Worker nicht unterstützt', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,16 +211,115 @@
|
|||||||
|
|
||||||
// Push Subscription Functions
|
// Push Subscription Functions
|
||||||
async function subscribeToPush() {
|
async function subscribeToPush() {
|
||||||
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
log('Push Subscription gestartet...', 'info');
|
||||||
|
|
||||||
|
// Check basic requirements
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
log('Service Worker nicht unterstützt', 'error');
|
||||||
|
updateStatus('Service Worker nicht unterstützt', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('PushManager' in window)) {
|
||||||
log('Push Manager nicht unterstützt', 'error');
|
log('Push Manager nicht unterstützt', 'error');
|
||||||
|
updateStatus('Push Manager nicht unterstützt', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check notification permission first
|
||||||
|
if (Notification.permission !== 'granted') {
|
||||||
|
log('Notification Permission nicht erteilt. Bitte zuerst "Berechtigung anfordern" klicken!', 'error');
|
||||||
|
updateStatus('Notification Permission erforderlich', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.ready;
|
log('Service Worker wird geladen...', 'info');
|
||||||
|
|
||||||
|
// First check if service worker is already registered
|
||||||
|
let registration;
|
||||||
|
const existingRegistrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
|
||||||
|
if (existingRegistrations.length > 0) {
|
||||||
|
log('Service Worker bereits registriert, verwende bestehende...', 'info');
|
||||||
|
registration = existingRegistrations[0];
|
||||||
|
} else {
|
||||||
|
log('Service Worker nicht registriert, registriere jetzt...', 'info');
|
||||||
|
registration = await navigator.serviceWorker.register('/sw.js');
|
||||||
|
log('Service Worker registriert', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for service worker to be ready with timeout
|
||||||
|
log('Warte auf Service Worker ready...', 'info');
|
||||||
|
|
||||||
|
// Check if service worker is active
|
||||||
|
if (registration.active) {
|
||||||
|
log('Service Worker ist bereits aktiv', 'success');
|
||||||
|
} else if (registration.installing && registration.installing.state) {
|
||||||
|
log('Service Worker wird installiert, warte...', 'info');
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker) {
|
||||||
|
installingWorker.addEventListener('statechange', () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log('Service Worker Installation abgeschlossen', 'success');
|
||||||
|
} else if (registration.waiting && registration.waiting.state) {
|
||||||
|
log('Service Worker wartet, aktiviere...', 'info');
|
||||||
|
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const waitingWorker = registration.waiting;
|
||||||
|
if (waitingWorker) {
|
||||||
|
waitingWorker.addEventListener('statechange', () => {
|
||||||
|
if (waitingWorker.state === 'activated') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log('Service Worker aktiviert', 'success');
|
||||||
|
} else {
|
||||||
|
log('Service Worker Status unbekannt, warte auf ready...', 'info');
|
||||||
|
try {
|
||||||
|
await navigator.serviceWorker.ready;
|
||||||
|
log('Service Worker bereit', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
log(`Service Worker ready fehlgeschlagen: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert VAPID key from base64url to ArrayBuffer
|
||||||
|
const vapidPublicKey = 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds';
|
||||||
|
log('VAPID Key wird konvertiert...', 'info');
|
||||||
|
|
||||||
|
let applicationServerKey;
|
||||||
|
try {
|
||||||
|
applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
|
||||||
|
log('VAPID Key konvertiert', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
log(`VAPID Key Konvertierung fehlgeschlagen: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Push Subscription wird erstellt...', 'info');
|
||||||
|
|
||||||
|
// Check if push manager is available
|
||||||
|
if (!registration.pushManager) {
|
||||||
|
throw new Error('Push Manager nicht verfügbar in diesem Service Worker');
|
||||||
|
}
|
||||||
|
|
||||||
const subscription = await registration.pushManager.subscribe({
|
const subscription = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: 'BJmNVx0C3XeVxeKGTP9c-Z4HcuZNmdk6QdiLocZgCmb-miCS0ESFO3W2TvJlRhhNAShV63pWA5p36BTVSetyTds'
|
applicationServerKey: applicationServerKey
|
||||||
});
|
});
|
||||||
|
|
||||||
currentSubscription = subscription;
|
currentSubscription = subscription;
|
||||||
@@ -206,6 +327,7 @@
|
|||||||
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
|
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
|
||||||
|
|
||||||
// Send to server
|
// Send to server
|
||||||
|
log('Subscription wird an Server gesendet...', 'info');
|
||||||
const response = await fetch('/api/v1/public/subscribe', {
|
const response = await fetch('/api/v1/public/subscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -214,17 +336,32 @@
|
|||||||
body: JSON.stringify(subscription)
|
body: JSON.stringify(subscription)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
log('Subscription erfolgreich an Server gesendet', 'success');
|
log('Subscription erfolgreich an Server gesendet', 'success');
|
||||||
|
log(`Player ID: ${result.playerId || 'anonymous'}`, 'success');
|
||||||
|
|
||||||
|
// Store the player ID for later use
|
||||||
|
if (result.playerId) {
|
||||||
|
localStorage.setItem('pushPlayerId', result.playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus('Push Subscription erfolgreich!', 'success');
|
||||||
// Store the subscription endpoint for later use
|
// Store the subscription endpoint for later use
|
||||||
localStorage.setItem('pushSubscriptionEndpoint', subscription.endpoint);
|
localStorage.setItem('pushSubscriptionEndpoint', subscription.endpoint);
|
||||||
} else {
|
} else {
|
||||||
log(`Server-Fehler: ${result.message}`, 'error');
|
log(`Server-Fehler: ${result.message}`, 'error');
|
||||||
|
updateStatus(`Server-Fehler: ${result.message}`, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
|
log(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
|
||||||
|
log(`Error Details: ${JSON.stringify(error)}`, 'error');
|
||||||
|
updateStatus(`Push Subscription fehlgeschlagen: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,42 +380,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkSubscription() {
|
async function checkSubscription() {
|
||||||
if ('serviceWorker' in navigator) {
|
log('Überprüfe Push Subscription...', 'info');
|
||||||
try {
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
if (!('serviceWorker' in navigator)) {
|
||||||
const subscription = await registration.pushManager.getSubscription();
|
log('Service Worker nicht unterstützt', 'error');
|
||||||
|
return;
|
||||||
if (subscription) {
|
}
|
||||||
currentSubscription = subscription;
|
|
||||||
log('Aktive Push Subscription gefunden', 'success');
|
try {
|
||||||
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
|
const registration = await navigator.serviceWorker.ready;
|
||||||
} else {
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
log('Keine Push Subscription gefunden', 'warning');
|
|
||||||
}
|
if (subscription) {
|
||||||
} catch (error) {
|
currentSubscription = subscription;
|
||||||
log(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
|
log('Aktive Push Subscription gefunden', 'success');
|
||||||
|
log(`Endpoint: ${subscription.endpoint.substring(0, 50)}...`);
|
||||||
|
updateStatus('Push Subscription aktiv', 'success');
|
||||||
|
} else {
|
||||||
|
log('Keine Push Subscription gefunden', 'warning');
|
||||||
|
updateStatus('Keine Push Subscription gefunden', 'warning');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
|
||||||
|
updateStatus(`Subscription Check fehlgeschlagen: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test Functions
|
// Test Functions
|
||||||
async function sendTestPush() {
|
async function sendTestPush() {
|
||||||
const message = document.getElementById('testMessage').value;
|
const message = document.getElementById('testMessage').value;
|
||||||
|
log('Test-Push wird gesendet...', 'info');
|
||||||
|
|
||||||
// First check if we have a subscription
|
// First check if we have a subscription
|
||||||
if (!currentSubscription) {
|
if (!currentSubscription) {
|
||||||
log('Keine Push Subscription gefunden. Bitte zuerst "Push abonnieren" klicken!', 'error');
|
log('Keine Push Subscription gefunden. Bitte zuerst "Push abonnieren" klicken!', 'error');
|
||||||
|
updateStatus('Keine Push Subscription gefunden', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the stored subscription endpoint as identifier
|
// Use the stored player ID from subscription
|
||||||
const storedEndpoint = localStorage.getItem('pushSubscriptionEndpoint');
|
const storedPlayerId = localStorage.getItem('pushPlayerId');
|
||||||
let userId = 'test-user';
|
let userId = 'test-user';
|
||||||
if (storedEndpoint) {
|
if (storedPlayerId) {
|
||||||
// Use the endpoint as a unique identifier
|
userId = storedPlayerId;
|
||||||
userId = storedEndpoint.split('/').pop().substring(0, 8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(`Sende Test-Push an Player ID: ${userId}`, 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/public/test-push', {
|
const response = await fetch('/api/v1/public/test-push', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -295,11 +443,14 @@
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
log('Test-Push erfolgreich gesendet', 'success');
|
log('Test-Push erfolgreich gesendet', 'success');
|
||||||
log(`An User ID: ${userId}`, 'success');
|
log(`An User ID: ${userId}`, 'success');
|
||||||
|
updateStatus('Test-Push erfolgreich gesendet!', 'success');
|
||||||
} else {
|
} else {
|
||||||
log(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
|
log(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
|
||||||
|
updateStatus(`Test-Push fehlgeschlagen: ${result.message}`, 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Test-Push Fehler: ${error.message}`, 'error');
|
log(`Test-Push Fehler: ${error.message}`, 'error');
|
||||||
|
updateStatus(`Test-Push Fehler: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +460,10 @@
|
|||||||
const notification = new Notification('🧪 Test Web Notification', {
|
const notification = new Notification('🧪 Test Web Notification', {
|
||||||
body: message,
|
body: message,
|
||||||
icon: '/pictures/icon-192.png',
|
icon: '/pictures/icon-192.png',
|
||||||
badge: '/pictures/icon-192.png'
|
badge: '/pictures/icon-192.png',
|
||||||
|
tag: 'test-notification',
|
||||||
|
requireInteraction: true,
|
||||||
|
silent: false
|
||||||
});
|
});
|
||||||
|
|
||||||
notification.onclick = function() {
|
notification.onclick = function() {
|
||||||
@@ -317,12 +471,52 @@
|
|||||||
notification.close();
|
notification.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Auto-close after 10 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.close();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
log('Web-Notification gesendet', 'success');
|
log('Web-Notification gesendet', 'success');
|
||||||
} else {
|
} else {
|
||||||
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
|
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Windows Desktop Notification (falls verfügbar)
|
||||||
|
function sendWindowsNotification() {
|
||||||
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
|
const message = document.getElementById('testMessage').value;
|
||||||
|
|
||||||
|
// Erstelle eine Windows-ähnliche Notification
|
||||||
|
const notification = new Notification('🏆 Ninja Cross - Achievement!', {
|
||||||
|
body: message,
|
||||||
|
icon: '/pictures/icon-192.png',
|
||||||
|
badge: '/pictures/icon-192.png',
|
||||||
|
tag: 'ninja-cross-achievement',
|
||||||
|
requireInteraction: true,
|
||||||
|
silent: false,
|
||||||
|
data: {
|
||||||
|
type: 'achievement',
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.onclick = function() {
|
||||||
|
window.focus();
|
||||||
|
notification.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-close after 15 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.close();
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
log('Windows-ähnliche Notification gesendet', 'success');
|
||||||
|
} else {
|
||||||
|
log('Web-Notifications nicht verfügbar oder nicht erlaubt', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getPushStatus() {
|
async function getPushStatus() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/public/push-status');
|
const response = await fetch('/api/v1/public/push-status');
|
||||||
@@ -340,11 +534,27 @@
|
|||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
|
console.log('Push Notification Test Seite geladen');
|
||||||
log('Push Notification Test Seite geladen');
|
log('Push Notification Test Seite geladen');
|
||||||
|
|
||||||
|
// Check if we're on HTTPS
|
||||||
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
|
log('WARNUNG: Push Notifications funktionieren nur über HTTPS!', 'error');
|
||||||
|
updateStatus('HTTPS erforderlich für Push Notifications', 'error');
|
||||||
|
} else {
|
||||||
|
log('HTTPS-Verbindung erkannt - Push Notifications möglich', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
checkServiceWorker();
|
checkServiceWorker();
|
||||||
checkPermission();
|
checkPermission();
|
||||||
checkSubscription();
|
checkSubscription();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also initialize on DOMContentLoaded as backup
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('DOM Content Loaded');
|
||||||
|
log('DOM Content Loaded - Initialisierung gestartet');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
125
routes/api.js
125
routes/api.js
@@ -2848,12 +2848,33 @@ router.get('/v1/public/best-times', async (req, res) => {
|
|||||||
// Subscribe to push notifications
|
// Subscribe to push notifications
|
||||||
router.post('/v1/public/subscribe', async (req, res) => {
|
router.post('/v1/public/subscribe', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { endpoint, keys } = req.body;
|
console.log('📱 Push subscription request received:', JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
|
const { endpoint, keys, playerId: requestPlayerId } = req.body;
|
||||||
const userId = req.session.userId || 'anonymous';
|
const userId = req.session.userId || 'anonymous';
|
||||||
|
|
||||||
// Generate a UUID for anonymous users or use existing UUID
|
// Validate required fields
|
||||||
|
if (!endpoint) {
|
||||||
|
console.error('❌ Missing endpoint in request');
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Endpoint ist erforderlich'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keys || !keys.p256dh || !keys.auth) {
|
||||||
|
console.error('❌ Missing keys in request:', keys);
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Push-Keys sind erforderlich'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use playerId from request if provided, otherwise generate one
|
||||||
let playerId;
|
let playerId;
|
||||||
if (userId === 'anonymous') {
|
if (requestPlayerId) {
|
||||||
|
playerId = requestPlayerId;
|
||||||
|
} else if (userId === 'anonymous') {
|
||||||
// Generate a random UUID for anonymous users
|
// Generate a random UUID for anonymous users
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
playerId = uuidv4();
|
playerId = uuidv4();
|
||||||
@@ -2861,6 +2882,8 @@ router.post('/v1/public/subscribe', async (req, res) => {
|
|||||||
playerId = userId;
|
playerId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`📱 Processing subscription for player: ${playerId}`);
|
||||||
|
|
||||||
// Store subscription in database
|
// Store subscription in database
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at)
|
INSERT INTO player_subscriptions (player_id, endpoint, p256dh, auth, created_at)
|
||||||
@@ -2890,7 +2913,8 @@ router.post('/v1/public/subscribe', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Push subscription erfolgreich gespeichert'
|
message: 'Push subscription erfolgreich gespeichert',
|
||||||
|
playerId: playerId
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error storing push subscription:', error);
|
console.error('Error storing push subscription:', error);
|
||||||
@@ -2902,6 +2926,42 @@ router.post('/v1/public/subscribe', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Unsubscribe from push notifications
|
||||||
|
router.post('/v1/public/unsubscribe', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { playerId } = req.body;
|
||||||
|
|
||||||
|
if (!playerId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Player ID erforderlich'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from push service
|
||||||
|
pushService.unsubscribe(playerId);
|
||||||
|
|
||||||
|
// Remove from database
|
||||||
|
await pool.query(`
|
||||||
|
DELETE FROM player_subscriptions
|
||||||
|
WHERE player_id = $1
|
||||||
|
`, [playerId]);
|
||||||
|
|
||||||
|
console.log(`User ${playerId} unsubscribed from push notifications`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Push subscription erfolgreich entfernt'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing push subscription:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Entfernen der Push Subscription'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Test push notification endpoint
|
// Test push notification endpoint
|
||||||
router.post('/v1/public/test-push', async (req, res) => {
|
router.post('/v1/public/test-push', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -2960,6 +3020,63 @@ router.get('/v1/public/push-status', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if notification was already sent today
|
||||||
|
router.get('/v1/public/notification-sent/:playerId/:type', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { playerId, type } = req.params;
|
||||||
|
const { achievementId, locationId } = req.query;
|
||||||
|
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM sent_notifications
|
||||||
|
WHERE player_id = $1
|
||||||
|
AND notification_type = $2
|
||||||
|
AND (achievement_id = $3 OR ($3 IS NULL AND achievement_id IS NULL))
|
||||||
|
AND (location_id = $4 OR ($4 IS NULL AND location_id IS NULL))
|
||||||
|
AND DATE(sent_at) = $5
|
||||||
|
`, [playerId, type, achievementId || null, locationId || null, today]);
|
||||||
|
|
||||||
|
const wasSent = parseInt(result.rows[0].count) > 0;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
wasSent: wasSent
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking notification status:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Prüfen des Notification-Status'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark notification as sent
|
||||||
|
router.post('/v1/public/notification-sent', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { playerId, notificationType, achievementId, locationId } = req.body;
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO sent_notifications (player_id, notification_type, achievement_id, location_id)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`, [playerId, notificationType, achievementId || null, locationId || null]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Notification als gesendet markiert'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking notification as sent:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Fehler beim Markieren der Notification'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== ANALYTICS HELPER FUNCTIONS ====================
|
// ==================== ANALYTICS HELPER FUNCTIONS ====================
|
||||||
|
|
||||||
async function getPerformanceTrends(playerId) {
|
async function getPerformanceTrends(playerId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user