diff --git a/achievements.js b/achievements.js new file mode 100644 index 0000000..a60f93d --- /dev/null +++ b/achievements.js @@ -0,0 +1,168 @@ +// Achievement system +const ACHIEVEMENTS = { + "first_visit": { + name: "Nováček", + description: "První návštěva na portálu", + icon: "fa-star", + color: "text-yellow-500" + }, + "frequent_visitor": { + name: "Pravidelný návštěvník", + description: "10 návštěv za měsíc", + icon: "fa-clock-rotate-left", + color: "text-blue-500", + threshold: 10, + period: "monthly" + }, + "power_user": { + name: "Power User", + description: "50 návštěv za měsíc", + icon: "fa-rocket", + color: "text-purple-500", + threshold: 50, + period: "monthly" + }, + "super_fan": { + name: "Super Fan", + description: "100 návštěv za měsíc", + icon: "fa-award", + color: "text-gold", + threshold: 100, + period: "monthly" + } +}; + +let achievementsEnabled = false; + +// Hidden toggle for achievements +function toggleAchievements() { + achievementsEnabled = !achievementsEnabled; + localStorage.setItem('achievementsEnabled', achievementsEnabled); + + if (achievementsEnabled) { + checkAchievements(); + showAchievements(); + } else { + hideAchievements(); + } +} + +// Check if user has earned achievements +async function checkAchievements() { + try { + const response = await fetch('/api/visitor-stats'); + const stats = await response.json(); + + // Check for monthly achievements + Object.values(ACHIEVEMENTS).forEach(achievement => { + if (achievement.period === "monthly" && stats.monthly_visits >= achievement.threshold) { + showAchievementToast(achievement); + } + }); + + // First visit achievement + if (stats.total_visits === 1) { + showAchievementToast(ACHIEVEMENTS.first_visit); + } + } catch (error) { + console.error('Error checking achievements:', error); + } +} + +// Show achievement toast +function showAchievementToast(achievement) { + const toast = document.createElement('div'); + toast.className = `fixed bottom-4 right-4 bg-white rounded-lg shadow-lg p-4 w-64 flex items-center ${achievement.color}`; + + toast.innerHTML = ` +
+

${achievement.name}

+

${achievement.description}

+
+ + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, 5000); +} + +// Show achievements display +function showAchievements() { + const achievementsDisplay = document.getElementById('achievementsDisplay'); + if (achievementsDisplay) { + achievementsDisplay.style.display = 'block'; + } +} + +// Hide achievements display +function hideAchievements() { + const achievementsDisplay = document.getElementById('achievementsDisplay'); + if (achievementsDisplay) { + achievementsDisplay.style.display = 'none'; + } +} + +// Initialize achievements +function initializeAchievements() { + // Check if achievements were enabled before + achievementsEnabled = JSON.parse(localStorage.getItem('achievementsEnabled') || 'false'); + + // Add hidden toggle button + const hiddenToggle = document.createElement('button'); + hiddenToggle.className = 'hidden'; + hiddenToggle.style.cssText = ` + position: fixed; + bottom: -100px; + right: -100px; + width: 50px; + height: 50px; + background: transparent; + border: none; + cursor: pointer; + transition: transform 0.3s; + `; + + hiddenToggle.onclick = () => { + toggleAchievements(); + hiddenToggle.style.transform = 'translate(-50px, -50px)'; + setTimeout(() => { + hiddenToggle.style.transform = ''; + }, 1000); + }; + + document.body.appendChild(hiddenToggle); + + // Check achievements when enabled + if (achievementsEnabled) { + checkAchievements(); + showAchievements(); + } +} + +// Add celebration animation +function celebrate() { + const confetti = document.createElement('div'); + confetti.className = 'absolute inset-0 pointer-events-none'; + confetti.innerHTML = ` +
+
+
+
+
+
+
+
+ `; + + document.body.appendChild(confetti); + + setTimeout(() => { + confetti.remove(); + }, 3000); +} + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', initializeAchievements); diff --git a/admin-dashboard.html b/admin-dashboard.html index ce1fd88..17c12a4 100644 --- a/admin-dashboard.html +++ b/admin-dashboard.html @@ -1067,6 +1067,29 @@

Vítejte v administraci

+ +
+

Statistiky návštěvností

+
+
+

Celkové návštěvy

+

0

+
+
+

Návštěvy dnes

+

0

+
+
+

Týdenní návštěvy

+

0

+
+
+

Měsíční návštěvy

+

0

+
+
+
+

Správa aplikací

@@ -4291,6 +4314,19 @@ function applyTemplate(templateId) { // Load apps when the page loads document.addEventListener('DOMContentLoaded', function() { + // Load visitor statistics + fetch('/api/visitor-stats') + .then(response => response.json()) + .then(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; + }) + .catch(error => { + console.error('Error loading visitor stats:', error); + }); + loadApps(); // Initialize banner image upload functionality diff --git a/index.html b/index.html index 4437cfa..134c108 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,35 @@ transform: translateY(-5px); box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } + + /* Achievements styles */ + #achievementsDisplay { + position: fixed; + top: 10px; + right: 10px; + background: white; + padding: 1rem; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + display: none; + z-index: 1000; + } + + .achievement-item { + display: flex; + align-items: center; + padding: 0.5rem; + border-bottom: 1px solid #eee; + } + + .achievement-item:last-child { + border-bottom: none; + } + + .achievement-icon { + font-size: 1.5rem; + margin-right: 0.75rem; + } - - \ No newline at end of file + +
+
+ +
+

Nováček

+

První návštěva na portálu

+
+
+
+ +
+

Pravidelný návštěvník

+

10 návštěv za měsíc

+
+
+
+ +
+

Power User

+

50 návštěv za měsíc

+
+
+
+ +
+

Super Fan

+

100 návštěv za měsíc

+
+
+
+ + + + + \ No newline at end of file diff --git a/main.go b/main.go index 27caec6..b569208 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,104 @@ import ( "gopkg.in/gomail.v2" ) +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"` +} + +const visitorStatsFile = "data/visitor_stats.json" + +func loadVisitorStats() (*VisitorStats, error) { + stats := &VisitorStats{ + TotalVisits: 0, + TodayVisits: 0, + MonthlyVisits: 0, + WeeklyVisits: 0, + LastVisit: time.Now(), + LastUpdated: time.Now(), + } + + data, err := os.ReadFile(visitorStatsFile) + if err != nil { + return stats, nil // Return default stats if file doesn't exist + } + + if err := json.Unmarshal(data, stats); err != nil { + return nil, fmt.Errorf("failed to unmarshal visitor stats: %v", err) + } + + return stats, nil +} + +func saveVisitorStats(stats *VisitorStats) error { + data, err := json.Marshal(stats) + if err != nil { + return fmt.Errorf("failed to marshal visitor stats: %v", err) + } + + return os.WriteFile(visitorStatsFile, data, 0644) +} + +func trackVisit(w http.ResponseWriter, r *http.Request) { + stats, err := loadVisitorStats() + if err != nil { + log.Printf("Error loading visitor stats: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + stats.TotalVisits++ + stats.LastVisit = time.Now() + + // Reset today's visits at midnight + if stats.LastUpdated.Day() != time.Now().Day() { + stats.TodayVisits = 1 + stats.WeeklyVisits++ + stats.MonthlyVisits++ + } else { + stats.TodayVisits++ + } + + // Reset weekly visits on Monday + if stats.LastUpdated.Weekday() != time.Now().Weekday() { + stats.WeeklyVisits = 1 + } + + // Reset monthly visits at the start of the month + if stats.LastUpdated.Month() != time.Now().Month() { + stats.MonthlyVisits = 1 + } + + stats.LastUpdated = time.Now() + + if err := saveVisitorStats(stats); err != nil { + log.Printf("Error saving visitor stats: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func getVisitorStats(w http.ResponseWriter, r *http.Request) { + stats, err := loadVisitorStats() + if err != nil { + log.Printf("Error loading visitor stats: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(stats); err != nil { + log.Printf("Error encoding visitor stats: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } +} + type App struct { ID string `json:"id"` Name string `json:"name"` @@ -73,6 +171,10 @@ func main() { r := mux.NewRouter() + // Visitor tracking endpoints + r.HandleFunc("/api/track-visit", trackVisit).Methods("GET") + r.HandleFunc("/api/visitor-stats", getVisitorStats).Methods("GET") + // Set up reverse proxy to kontakt service kontaktURL, _ := url.Parse("http://webportal:8080") kontaktProxy := httputil.NewSingleHostReverseProxy(kontaktURL)