diff --git a/achievements.js b/achievements.js
index 7cdacf8..d28daa9 100644
--- a/achievements.js
+++ b/achievements.js
@@ -4,7 +4,13 @@ const ACHIEVEMENTS = {
name: "Nováček",
description: "První návštěva na portálu",
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": {
name: "Pravidelný návštěvník",
@@ -12,7 +18,13 @@ const ACHIEVEMENTS = {
icon: "fa-clock-rotate-left",
color: "text-blue-500",
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": {
name: "Power User",
@@ -20,7 +32,13 @@ const ACHIEVEMENTS = {
icon: "fa-rocket",
color: "text-purple-500",
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": {
name: "Super Fan",
@@ -28,10 +46,36 @@ const ACHIEVEMENTS = {
icon: "fa-award",
color: "text-gold",
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;
// Hidden toggle for achievements
@@ -56,19 +100,69 @@ async function checkAchievements() {
// Check for monthly achievements
Object.values(ACHIEVEMENTS).forEach(achievement => {
if (achievement.period === "monthly" && stats.monthly_visits >= achievement.threshold) {
- showAchievementToast(achievement);
+ unlockAchievement(achievement);
}
});
// First visit achievement
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) {
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 = `
+
+
+
${achievement.name}
+
${achievement.description}
+
+ `;
+
+ achievementsDisplay.appendChild(achievementItem);
+ });
+
+ achievementsDisplay.style.display = 'block';
+ }
+}
+
// Show achievement toast
function showAchievementToast(achievement) {
const toast = document.createElement('div');
@@ -144,9 +238,6 @@ function initializeAchievements() {
`;
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
toggleAchievements();
diff --git a/admin-dashboard.html b/admin-dashboard.html
index 17c12a4..1ccb2ea 100644
--- a/admin-dashboard.html
+++ b/admin-dashboard.html
@@ -1068,8 +1068,10 @@
Vítejte v administraci
-
+
Statistiky návštěvností
+
+
Celkové návštěvy
@@ -1088,6 +1090,44 @@
0
+
+
+
+
Podrobné statistiky
+
+
+
+
+
+
+
+
Nejaktivnější hodiny
+
+
+
+
+
+
+
+
+
Unikátní návštěvníci
+
+
+
@@ -4318,10 +4358,72 @@ document.addEventListener('DOMContentLoaded', function() {
fetch('/api/visitor-stats')
.then(response => response.json())
.then(stats => {
+ // Basic stats
document.getElementById('totalVisits').textContent = stats.total_visits;
document.getElementById('todayVisits').textContent = stats.today_visits;
document.getElementById('weeklyVisits').textContent = stats.weekly_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]) => `
+
+ ${browser}
+ ${count}
+
+ `).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]) => `
+
+ ${os}
+ ${count}
+
+ `).join('');
+
+ // Active hours
+ const activeHours = document.getElementById('activeHours');
+ activeHours.innerHTML = stats.most_active_hours
+ .sort((a, b) => b.count - a.count)
+ .map(hour => `
+
+ ${hour.hour}:00
+ ${hour.count}
+
+ `).join('');
+
+ // Active days
+ const activeDays = document.getElementById('activeDays');
+ activeDays.innerHTML = stats.most_active_days
+ .sort((a, b) => b.count - a.count)
+ .map(day => `
+
+ ${day.day}
+ ${day.count}
+
+ `).join('');
+
+ // Unique visitors
+ const uniqueVisitors = document.getElementById('uniqueVisitors');
+ uniqueVisitors.innerHTML = Object.entries(stats.unique_visitors)
+ .map(([id, visitor]) => `
+
+
+
${visitor.ip}
+
${visitor.user_agent}
+
+
+ ${visitor.visits} návštěv
+ |
+ ${visitor.last_visit.toLocaleString()}
+
+
+ `).join('');
})
.catch(error => {
console.error('Error loading visitor stats:', error);
diff --git a/main.go b/main.go
index b569208..2883953 100644
--- a/main.go
+++ b/main.go
@@ -20,12 +20,30 @@ import (
)
type VisitorStats struct {
- TotalVisits int `json:"total_visits"`
- TodayVisits int `json:"today_visits"`
- LastVisit time.Time `json:"last_visit"`
- MonthlyVisits int `json:"monthly_visits"`
- WeeklyVisits int `json:"weekly_visits"`
- LastUpdated time.Time `json:"last_updated"`
+ TotalVisits int `json:"total_visits"`
+ TodayVisits int `json:"today_visits"`
+ LastVisit time.Time `json:"last_visit"`
+ MonthlyVisits int `json:"monthly_visits"`
+ WeeklyVisits int `json:"weekly_visits"`
+ 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"
@@ -69,6 +87,86 @@ func trackVisit(w http.ResponseWriter, r *http.Request) {
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.LastVisit = time.Now()
@@ -102,6 +200,19 @@ func trackVisit(w http.ResponseWriter, r *http.Request) {
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) {
stats, err := loadVisitorStats()
if err != nil {