mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-04 04:22:58 +00:00
test
This commit is contained in:
+168
@@ -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 = `
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="font-bold text-lg">${achievement.name}</h3>
|
||||||
|
<p class="text-gray-600">${achievement.description}</p>
|
||||||
|
</div>
|
||||||
|
<i class="fas ${achievement.icon} text-2xl ml-4"></i>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="w-64 h-64 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="w-48 h-48 rounded-full bg-gradient-to-r from-blue-500 to-cyan-500 opacity-25"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(confetti);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
confetti.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeAchievements);
|
||||||
@@ -1067,6 +1067,29 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Vítejte v administraci</h2>
|
<h2>Vítejte v administraci</h2>
|
||||||
|
|
||||||
|
<!-- Visitor Statistics Section -->
|
||||||
|
<div class="card" style="margin: 2rem auto; max-width: 1000px;">
|
||||||
|
<h3>Statistiky návštěvností</h3>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||||
|
<div class="bg-blue-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-blue-700 font-semibold">Celkové návštěvy</h4>
|
||||||
|
<p class="text-2xl font-bold mt-2" id="totalVisits">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-green-700 font-semibold">Návštěvy dnes</h4>
|
||||||
|
<p class="text-2xl font-bold mt-2" id="todayVisits">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-yellow-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-yellow-700 font-semibold">Týdenní návštěvy</h4>
|
||||||
|
<p class="text-2xl font-bold mt-2" id="weeklyVisits">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-purple-50 p-4 rounded-lg">
|
||||||
|
<h4 class="text-purple-700 font-semibold">Měsíční návštěvy</h4>
|
||||||
|
<p class="text-2xl font-bold mt-2" id="monthlyVisits">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Apps Management Section -->
|
<!-- Apps Management Section -->
|
||||||
<div class="card" style="margin: 2rem auto; max-width: 1000px;" id="aplikace">
|
<div class="card" style="margin: 2rem auto; max-width: 1000px;" id="aplikace">
|
||||||
<h3>Správa aplikací</h3>
|
<h3>Správa aplikací</h3>
|
||||||
@@ -4291,6 +4314,19 @@ function applyTemplate(templateId) {
|
|||||||
|
|
||||||
// Load apps when the page loads
|
// Load apps when the page loads
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
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();
|
loadApps();
|
||||||
|
|
||||||
// Initialize banner image upload functionality
|
// Initialize banner image upload functionality
|
||||||
|
|||||||
+75
-2
@@ -14,6 +14,35 @@
|
|||||||
transform: translateY(-5px);
|
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);
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
// Function to get icon based on app description or name
|
// Function to get icon based on app description or name
|
||||||
@@ -588,5 +617,49 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
<!-- Achievements display -->
|
||||||
</html>
|
<div id="achievementsDisplay">
|
||||||
|
<div class="achievement-item">
|
||||||
|
<i class="fas fa-star achievement-icon text-yellow-500"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">Nováček</h4>
|
||||||
|
<p class="text-sm text-gray-600">První návštěva na portálu</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-item">
|
||||||
|
<i class="fas fa-clock-rotate-left achievement-icon text-blue-500"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">Pravidelný návštěvník</h4>
|
||||||
|
<p class="text-sm text-gray-600">10 návštěv za měsíc</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-item">
|
||||||
|
<i class="fas fa-rocket achievement-icon text-purple-500"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">Power User</h4>
|
||||||
|
<p class="text-sm text-gray-600">50 návštěv za měsíc</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-item">
|
||||||
|
<i class="fas fa-award achievement-icon text-gold"></i>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">Super Fan</h4>
|
||||||
|
<p class="text-sm text-gray-600">100 návštěv za měsíc</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="achievements.js"></script>
|
||||||
|
<script>
|
||||||
|
// Track page visit when the page loads
|
||||||
|
fetch('/api/track-visit', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error tracking visit:', error);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -19,6 +19,104 @@ import (
|
|||||||
"gopkg.in/gomail.v2"
|
"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 {
|
type App struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -73,6 +171,10 @@ func main() {
|
|||||||
|
|
||||||
r := mux.NewRouter()
|
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
|
// Set up reverse proxy to kontakt service
|
||||||
kontaktURL, _ := url.Parse("http://webportal:8080")
|
kontaktURL, _ := url.Parse("http://webportal:8080")
|
||||||
kontaktProxy := httputil.NewSingleHostReverseProxy(kontaktURL)
|
kontaktProxy := httputil.NewSingleHostReverseProxy(kontaktURL)
|
||||||
|
|||||||
Reference in New Issue
Block a user