mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-05 04:52:58 +00:00
test2
This commit is contained in:
+100
-9
@@ -4,7 +4,13 @@ const ACHIEVEMENTS = {
|
|||||||
name: "Nováček",
|
name: "Nováček",
|
||||||
description: "První návštěva na portálu",
|
description: "První návštěva na portálu",
|
||||||
icon: "fa-star",
|
icon: "fa-star",
|
||||||
color: "text-yellow-500"
|
color: "text-yellow-500",
|
||||||
|
theme: {
|
||||||
|
backgroundColor: "bg-yellow-50",
|
||||||
|
textColor: "text-yellow-700",
|
||||||
|
borderColor: "border-yellow-200",
|
||||||
|
hoverColor: "hover:bg-yellow-100"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"frequent_visitor": {
|
"frequent_visitor": {
|
||||||
name: "Pravidelný návštěvník",
|
name: "Pravidelný návštěvník",
|
||||||
@@ -12,7 +18,13 @@ const ACHIEVEMENTS = {
|
|||||||
icon: "fa-clock-rotate-left",
|
icon: "fa-clock-rotate-left",
|
||||||
color: "text-blue-500",
|
color: "text-blue-500",
|
||||||
threshold: 10,
|
threshold: 10,
|
||||||
period: "monthly"
|
period: "monthly",
|
||||||
|
theme: {
|
||||||
|
backgroundColor: "bg-blue-50",
|
||||||
|
textColor: "text-blue-700",
|
||||||
|
borderColor: "border-blue-200",
|
||||||
|
hoverColor: "hover:bg-blue-100"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"power_user": {
|
"power_user": {
|
||||||
name: "Power User",
|
name: "Power User",
|
||||||
@@ -20,7 +32,13 @@ const ACHIEVEMENTS = {
|
|||||||
icon: "fa-rocket",
|
icon: "fa-rocket",
|
||||||
color: "text-purple-500",
|
color: "text-purple-500",
|
||||||
threshold: 50,
|
threshold: 50,
|
||||||
period: "monthly"
|
period: "monthly",
|
||||||
|
theme: {
|
||||||
|
backgroundColor: "bg-purple-50",
|
||||||
|
textColor: "text-purple-700",
|
||||||
|
borderColor: "border-purple-200",
|
||||||
|
hoverColor: "hover:bg-purple-100"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"super_fan": {
|
"super_fan": {
|
||||||
name: "Super Fan",
|
name: "Super Fan",
|
||||||
@@ -28,10 +46,36 @@ const ACHIEVEMENTS = {
|
|||||||
icon: "fa-award",
|
icon: "fa-award",
|
||||||
color: "text-gold",
|
color: "text-gold",
|
||||||
threshold: 100,
|
threshold: 100,
|
||||||
period: "monthly"
|
period: "monthly",
|
||||||
|
theme: {
|
||||||
|
backgroundColor: "bg-yellow-50",
|
||||||
|
textColor: "text-yellow-700",
|
||||||
|
borderColor: "border-yellow-200",
|
||||||
|
hoverColor: "hover:bg-yellow-100"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track unlocked achievements
|
||||||
|
let unlockedAchievements = new Set();
|
||||||
|
|
||||||
|
// Store current theme
|
||||||
|
let currentTheme = {
|
||||||
|
backgroundColor: "bg-white",
|
||||||
|
textColor: "text-gray-800",
|
||||||
|
borderColor: "border-gray-200",
|
||||||
|
hoverColor: "hover:bg-gray-50"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply theme to all cards
|
||||||
|
function applyTheme() {
|
||||||
|
const cards = document.querySelectorAll('.card');
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.className = card.className.split(' ').filter(cls => !cls.startsWith('bg-') && !cls.startsWith('text-') && !cls.startsWith('border-') && !cls.startsWith('hover:')).join(' ');
|
||||||
|
card.className += ` ${currentTheme.backgroundColor} ${currentTheme.textColor} ${currentTheme.borderColor} ${currentTheme.hoverColor}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let achievementsEnabled = false;
|
let achievementsEnabled = false;
|
||||||
|
|
||||||
// Hidden toggle for achievements
|
// Hidden toggle for achievements
|
||||||
@@ -56,19 +100,69 @@ async function checkAchievements() {
|
|||||||
// Check for monthly achievements
|
// Check for monthly achievements
|
||||||
Object.values(ACHIEVEMENTS).forEach(achievement => {
|
Object.values(ACHIEVEMENTS).forEach(achievement => {
|
||||||
if (achievement.period === "monthly" && stats.monthly_visits >= achievement.threshold) {
|
if (achievement.period === "monthly" && stats.monthly_visits >= achievement.threshold) {
|
||||||
showAchievementToast(achievement);
|
unlockAchievement(achievement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// First visit achievement
|
// First visit achievement
|
||||||
if (stats.total_visits === 1) {
|
if (stats.total_visits === 1) {
|
||||||
showAchievementToast(ACHIEVEMENTS.first_visit);
|
unlockAchievement(ACHIEVEMENTS.first_visit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply highest unlocked achievement theme
|
||||||
|
const unlocked = Array.from(unlockedAchievements);
|
||||||
|
if (unlocked.length > 0) {
|
||||||
|
const highestAchievement = unlocked[unlocked.length - 1];
|
||||||
|
currentTheme = ACHIEVEMENTS[highestAchievement].theme;
|
||||||
|
applyTheme();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking achievements:', error);
|
console.error('Error checking achievements:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlock achievement and show toast
|
||||||
|
function unlockAchievement(achievement) {
|
||||||
|
const achievementId = Object.keys(ACHIEVEMENTS).find(key =>
|
||||||
|
ACHIEVEMENTS[key].name === achievement.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!unlockedAchievements.has(achievementId)) {
|
||||||
|
unlockedAchievements.add(achievementId);
|
||||||
|
showAchievementToast(achievement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show only unlocked achievements
|
||||||
|
function showAchievements() {
|
||||||
|
const achievementsDisplay = document.getElementById('achievementsDisplay');
|
||||||
|
if (achievementsDisplay) {
|
||||||
|
// Clear existing achievements
|
||||||
|
achievementsDisplay.innerHTML = '';
|
||||||
|
|
||||||
|
// Show only unlocked achievements
|
||||||
|
Array.from(unlockedAchievements).forEach(achievementId => {
|
||||||
|
const achievement = ACHIEVEMENTS[achievementId];
|
||||||
|
const achievementItem = document.createElement('div');
|
||||||
|
achievementItem.className = 'achievement-item flex items-center p-3 rounded-lg mb-2';
|
||||||
|
achievementItem.style.backgroundColor = achievement.theme.backgroundColor;
|
||||||
|
achievementItem.style.color = achievement.theme.textColor;
|
||||||
|
|
||||||
|
achievementItem.innerHTML = `
|
||||||
|
<i class="fas ${achievement.icon} achievement-icon ${achievement.color}"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">${achievement.name}</h4>
|
||||||
|
<p class="text-sm">${achievement.description}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
achievementsDisplay.appendChild(achievementItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
achievementsDisplay.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show achievement toast
|
// Show achievement toast
|
||||||
function showAchievementToast(achievement) {
|
function showAchievementToast(achievement) {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
@@ -144,9 +238,6 @@ function initializeAchievements() {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(achievementToast);
|
document.body.appendChild(achievementToast);
|
||||||
|
|
||||||
// Play achievement sound
|
|
||||||
const audio = new Audio('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3');
|
|
||||||
audio.play();
|
|
||||||
|
|
||||||
// Enable achievements
|
// Enable achievements
|
||||||
toggleAchievements();
|
toggleAchievements();
|
||||||
|
|||||||
+103
-1
@@ -1068,8 +1068,10 @@
|
|||||||
<h2>Vítejte v administraci</h2>
|
<h2>Vítejte v administraci</h2>
|
||||||
|
|
||||||
<!-- Visitor Statistics Section -->
|
<!-- Visitor Statistics Section -->
|
||||||
<div class="card" style="margin: 2rem auto; max-width: 1000px;">
|
<div class="card" style="margin: 2rem auto; max-width: 1200px;">
|
||||||
<h3>Statistiky návštěvností</h3>
|
<h3>Statistiky návštěvností</h3>
|
||||||
|
|
||||||
|
<!-- Basic Stats -->
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||||
<div class="bg-blue-50 p-4 rounded-lg">
|
<div class="bg-blue-50 p-4 rounded-lg">
|
||||||
<h4 class="text-blue-700 font-semibold">Celkové návštěvy</h4>
|
<h4 class="text-blue-700 font-semibold">Celkové návštěvy</h4>
|
||||||
@@ -1088,6 +1090,44 @@
|
|||||||
<p class="text-2xl font-bold mt-2" id="monthlyVisits">0</p>
|
<p class="text-2xl font-bold mt-2" id="monthlyVisits">0</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Detailed Stats -->
|
||||||
|
<div class="mt-6">
|
||||||
|
<h4 class="text-lg font-semibold mb-4">Podrobné statistiky</h4>
|
||||||
|
|
||||||
|
<!-- Browser Stats -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h5 class="font-semibold mb-2">Prohlížeče</h5>
|
||||||
|
<div id="browserStats" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OS Stats -->
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h5 class="font-semibold mb-2">Operační systémy</h5>
|
||||||
|
<div id="osStats" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Hours and Days -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h5 class="font-semibold mb-2">Nejaktivnější hodiny</h5>
|
||||||
|
<div id="activeHours" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h5 class="font-semibold mb-2">Nejaktivnější dny</h5>
|
||||||
|
<div id="activeDays" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Unique Visitors -->
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow">
|
||||||
|
<h5 class="font-semibold mb-2">Unikátní návštěvníci</h5>
|
||||||
|
<div id="uniqueVisitors" class="space-y-2 max-h-96 overflow-y-auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Apps Management Section -->
|
<!-- Apps Management Section -->
|
||||||
@@ -4318,10 +4358,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
fetch('/api/visitor-stats')
|
fetch('/api/visitor-stats')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(stats => {
|
.then(stats => {
|
||||||
|
// Basic stats
|
||||||
document.getElementById('totalVisits').textContent = stats.total_visits;
|
document.getElementById('totalVisits').textContent = stats.total_visits;
|
||||||
document.getElementById('todayVisits').textContent = stats.today_visits;
|
document.getElementById('todayVisits').textContent = stats.today_visits;
|
||||||
document.getElementById('weeklyVisits').textContent = stats.weekly_visits;
|
document.getElementById('weeklyVisits').textContent = stats.weekly_visits;
|
||||||
document.getElementById('monthlyVisits').textContent = stats.monthly_visits;
|
document.getElementById('monthlyVisits').textContent = stats.monthly_visits;
|
||||||
|
|
||||||
|
// Browser stats
|
||||||
|
const browserStats = document.getElementById('browserStats');
|
||||||
|
browserStats.innerHTML = Object.entries(stats.browser_stats)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([browser, count]) => `
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm">${browser}</span>
|
||||||
|
<span class="text-sm text-gray-600">${count}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// OS stats
|
||||||
|
const osStats = document.getElementById('osStats');
|
||||||
|
osStats.innerHTML = Object.entries(stats.os_stats)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([os, count]) => `
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm">${os}</span>
|
||||||
|
<span class="text-sm text-gray-600">${count}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Active hours
|
||||||
|
const activeHours = document.getElementById('activeHours');
|
||||||
|
activeHours.innerHTML = stats.most_active_hours
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.map(hour => `
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm">${hour.hour}:00</span>
|
||||||
|
<span class="text-sm text-gray-600">${hour.count}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Active days
|
||||||
|
const activeDays = document.getElementById('activeDays');
|
||||||
|
activeDays.innerHTML = stats.most_active_days
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.map(day => `
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm">${day.day}</span>
|
||||||
|
<span class="text-sm text-gray-600">${day.count}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Unique visitors
|
||||||
|
const uniqueVisitors = document.getElementById('uniqueVisitors');
|
||||||
|
uniqueVisitors.innerHTML = Object.entries(stats.unique_visitors)
|
||||||
|
.map(([id, visitor]) => `
|
||||||
|
<div class="flex justify-between items-center p-2 hover:bg-gray-50 rounded">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-sm">${visitor.ip}</div>
|
||||||
|
<div class="text-xs text-gray-500">${visitor.user_agent}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-xs text-gray-600">${visitor.visits} návštěv</span>
|
||||||
|
<span class="text-xs text-gray-400">|</span>
|
||||||
|
<span class="text-xs text-gray-600">${visitor.last_visit.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error loading visitor stats:', error);
|
console.error('Error loading visitor stats:', error);
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ type VisitorStats struct {
|
|||||||
MonthlyVisits int `json:"monthly_visits"`
|
MonthlyVisits int `json:"monthly_visits"`
|
||||||
WeeklyVisits int `json:"weekly_visits"`
|
WeeklyVisits int `json:"weekly_visits"`
|
||||||
LastUpdated time.Time `json:"last_updated"`
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
UniqueVisitors map[string]struct {
|
||||||
|
FirstVisit time.Time `json:"first_visit"`
|
||||||
|
LastVisit time.Time `json:"last_visit"`
|
||||||
|
Visits int `json:"visits"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
} `json:"unique_visitors"`
|
||||||
|
MostActiveHours []struct {
|
||||||
|
Hour int `json:"hour"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
} `json:"most_active_hours"`
|
||||||
|
MostActiveDays []struct {
|
||||||
|
Day string `json:"day"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
} `json:"most_active_days"`
|
||||||
|
BrowserStats map[string]int `json:"browser_stats"`
|
||||||
|
OSStats map[string]int `json:"os_stats"`
|
||||||
|
ReferrerStats map[string]int `json:"referrer_stats"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const visitorStatsFile = "data/visitor_stats.json"
|
const visitorStatsFile = "data/visitor_stats.json"
|
||||||
@@ -69,6 +87,86 @@ func trackVisit(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get visitor ID (using IP and User-Agent)
|
||||||
|
visitorID := fmt.Sprintf("%s-%s", r.RemoteAddr, r.UserAgent())
|
||||||
|
|
||||||
|
// Track unique visitor
|
||||||
|
if stats.UniqueVisitors == nil {
|
||||||
|
stats.UniqueVisitors = make(map[string]struct {
|
||||||
|
FirstVisit time.Time
|
||||||
|
LastVisit time.Time
|
||||||
|
Visits int
|
||||||
|
IP string
|
||||||
|
UserAgent string
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor := stats.UniqueVisitors[visitorID]
|
||||||
|
if visitor.Visits == 0 {
|
||||||
|
visitor.FirstVisit = time.Now()
|
||||||
|
}
|
||||||
|
visitor.LastVisit = time.Now()
|
||||||
|
visitor.Visits++
|
||||||
|
visitor.IP = r.RemoteAddr
|
||||||
|
visitor.UserAgent = r.UserAgent()
|
||||||
|
stats.UniqueVisitors[visitorID] = visitor
|
||||||
|
|
||||||
|
// Track browser
|
||||||
|
if stats.BrowserStats == nil {
|
||||||
|
stats.BrowserStats = make(map[string]int)
|
||||||
|
}
|
||||||
|
browser := r.Header.Get("User-Agent")
|
||||||
|
stats.BrowserStats[browser]++
|
||||||
|
|
||||||
|
// Track OS
|
||||||
|
if stats.OSStats == nil {
|
||||||
|
stats.OSStats = make(map[string]int)
|
||||||
|
}
|
||||||
|
os := getOSFromUserAgent(r.UserAgent())
|
||||||
|
stats.OSStats[os]++
|
||||||
|
|
||||||
|
// Track referrer
|
||||||
|
if stats.ReferrerStats == nil {
|
||||||
|
stats.ReferrerStats = make(map[string]int)
|
||||||
|
}
|
||||||
|
referrer := r.Header.Get("Referer")
|
||||||
|
stats.ReferrerStats[referrer]++
|
||||||
|
|
||||||
|
// Track active hours
|
||||||
|
hour := time.Now().Hour()
|
||||||
|
foundHour := false
|
||||||
|
for i := range stats.MostActiveHours {
|
||||||
|
if stats.MostActiveHours[i].Hour == hour {
|
||||||
|
stats.MostActiveHours[i].Count++
|
||||||
|
foundHour = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHour {
|
||||||
|
stats.MostActiveHours = append(stats.MostActiveHours, struct {
|
||||||
|
Hour int `json:"hour"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}{Hour: hour, Count: 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track active days
|
||||||
|
day := time.Now().Weekday().String()
|
||||||
|
foundDay := false
|
||||||
|
for i := range stats.MostActiveDays {
|
||||||
|
if stats.MostActiveDays[i].Day == day {
|
||||||
|
stats.MostActiveDays[i].Count++
|
||||||
|
foundDay = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundDay {
|
||||||
|
stats.MostActiveDays = append(stats.MostActiveDays, struct {
|
||||||
|
Day string `json:"day"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}{Day: day, Count: 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update basic stats
|
||||||
stats.TotalVisits++
|
stats.TotalVisits++
|
||||||
stats.LastVisit = time.Now()
|
stats.LastVisit = time.Now()
|
||||||
|
|
||||||
@@ -102,6 +200,19 @@ func trackVisit(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to extract OS from User-Agent
|
||||||
|
func getOSFromUserAgent(userAgent string) string {
|
||||||
|
if strings.Contains(userAgent, "Windows") {
|
||||||
|
return "Windows"
|
||||||
|
} else if strings.Contains(userAgent, "Mac") {
|
||||||
|
return "MacOS"
|
||||||
|
} else if strings.Contains(userAgent, "Linux") {
|
||||||
|
return "Linux"
|
||||||
|
} else {
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getVisitorStats(w http.ResponseWriter, r *http.Request) {
|
func getVisitorStats(w http.ResponseWriter, r *http.Request) {
|
||||||
stats, err := loadVisitorStats()
|
stats, err := loadVisitorStats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user