diff --git a/admin-dashboard.html b/admin-dashboard.html index 323d101..5d03667 100644 --- a/admin-dashboard.html +++ b/admin-dashboard.html @@ -172,6 +172,122 @@ color: #666; margin-bottom: 0; } + + /* Form Actions */ + .form-actions { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #eee; + display: flex; + justify-content: flex-end; + gap: 1rem; + } + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1.25rem; + border: 1px solid transparent; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + line-height: 1.5; + cursor: pointer; + transition: all 0.2s ease-in-out; + text-decoration: none; + gap: 0.5rem; + } + + .btn i { + font-size: 0.9em; + } + + .btn-primary { + background-color: #4a6cf7; + color: white; + border-color: #4a6cf7; + } + + .btn-primary:hover { + background-color: #3a5ce4; + border-color: #3a5ce4; + } + + .btn-secondary { + background-color: #6c757d; + color: white; + border-color: #6c757d; + } + + .btn-secondary:hover { + background-color: #5a6268; + border-color: #545b62; + } + + .btn-danger { + background-color: #dc3545; + color: white; + border-color: #dc3545; + } + + .btn-danger:hover { + background-color: #c82333; + border-color: #bd2130; + } + + /* Notifications */ + .notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 4px; + color: white; + display: flex; + align-items: center; + gap: 10px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + opacity: 0; + transform: translateY(-20px); + animation: slideIn 0.3s ease-out forwards; + } + + .notification i { + font-size: 1.2em; + } + + .notification.success { + background-color: #28a745; + } + + .notification.error { + background-color: #dc3545; + } + + .notification.warning { + background-color: #ffc107; + color: #212529; + } + + .fade-out { + animation: fadeOut 0.3s ease-out forwards; + } + + @keyframes slideIn { + to { + opacity: 1; + transform: translateY(0); + } + } + + @keyframes fadeOut { + to { + opacity: 0; + transform: translateY(-20px); + } + } @@ -186,95 +302,106 @@

Správa banneru

-
- - -
- -
- - -
- -
- - -
- - +
+
+ +
- - -

Styl banneru

- -
- -
- -
- -
- - -
+ +
+ + + +
+ + +
+
-
- -
- -
- - -
+ +
+
-
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -

Předvolby stylů

-
-
Informační
-
Upozornění
-
Úspěch
-
Chyba
-
+ +

Styl

+ +
+ + + +
+
+ +
+ + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

Předvolby stylů

+
+
Info
+
Upozornění
+
Úspěch
+
Chyba
+
+ + + +
+ +
+
- -
@@ -391,13 +514,26 @@ const banner = await response.json(); // Update form fields - bannerText.value = banner.text || ''; - bannerVisible.checked = banner.style.isVisible !== false; - bannerBgColor.value = banner.style.backgroundColor || '#f8d7da'; - bannerTextColor.value = banner.style.textColor || '#721c24'; - bannerTextAlign.value = banner.style.textAlign || 'center'; - bannerFontSize.value = parseInt(banner.style.fontSize || '16'); - bannerPadding.value = parseInt(banner.style.padding || '10'); + document.getElementById('bannerText').value = banner.text || ''; + document.getElementById('bannerLink').value = banner.link || ''; + document.getElementById('bannerVisible').checked = banner.style.isVisible !== false; + document.getElementById('bannerBgColor').value = banner.style.backgroundColor || '#f8d7da'; + document.getElementById('bannerBgColorPicker').value = banner.style.backgroundColor || '#f8d7da'; + document.getElementById('bannerTextColor').value = banner.style.textColor || '#721c24'; + document.getElementById('bannerTextColorPicker').value = banner.style.textColor || '#721c24'; + document.getElementById('bannerTextAlign').value = banner.style.textAlign || 'center'; + document.getElementById('bannerFontSize').value = banner.style.fontSize ? banner.style.fontSize.replace('px', '') : '18'; + document.getElementById('bannerPadding').value = banner.style.padding ? banner.style.padding.replace('px', '') : '20'; + document.getElementById('bannerMargin').value = banner.style.margin ? banner.style.margin.replace('px', '') : '20'; + document.getElementById('bannerBorderRadius').value = banner.style.borderRadius ? banner.style.borderRadius.replace('px', '') : '8'; + + // Handle image + if (banner.image) { + currentImage = banner.image; + document.getElementById('imagePreview').src = currentImage; + document.getElementById('imagePreviewContainer').style.display = 'block'; + document.getElementById('removeImageBtn').style.display = 'inline-block'; + } updateColorPreviews(); updateBannerPreview(); @@ -409,33 +545,85 @@ } // Save banner - async function saveBanner() { + async function saveBanner(event) { + event.preventDefault(); + + const form = document.getElementById('bannerForm'); + const formData = new FormData(form); + const saveBtn = document.getElementById('saveBannerBtn'); + const originalBtnText = saveBtn.innerHTML; + + // Update button state + saveBtn.disabled = true; + saveBtn.innerHTML = ' Ukládám...'; + try { - const bannerData = { - text: bannerText.value, - style: { - backgroundColor: bannerBgColor.value, - textColor: bannerTextColor.value, - textAlign: bannerTextAlign.value, - fontSize: `${bannerFontSize.value}px`, - padding: `${bannerPadding.value}px`, - isVisible: bannerVisible.checked - } - }; - + // Add style properties to form data + formData.append('style[backgroundColor]', document.getElementById('bannerBgColor').value); + formData.append('style[textColor]', document.getElementById('bannerTextColor').value); + formData.append('style[textAlign]', document.getElementById('bannerTextAlign').value); + formData.append('style[fontSize]', document.getElementById('bannerFontSize').value + 'px'); + formData.append('style[padding]', document.getElementById('bannerPadding').value + 'px'); + formData.append('style[margin]', document.getElementById('bannerMargin').value + 'px'); + formData.append('style[borderRadius]', document.getElementById('bannerBorderRadius').value + 'px'); + formData.append('style[isVisible]', document.getElementById('bannerVisible').checked); + const response = await fetch('/api/banner/update', { method: 'POST', - body: JSON.stringify(bannerData) + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: formData }); - if (!response.ok) throw new Error('Failed to save banner'); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || 'Chyba při ukládání banneru'); + } + + const result = await response.json(); + + // Show success message + const notification = document.createElement('div'); + notification.className = 'notification success'; + notification.innerHTML = ' Banner byl úspěšně uložen'; + document.body.appendChild(notification); + + // Remove notification after 3 seconds + setTimeout(() => { + notification.classList.add('fade-out'); + setTimeout(() => notification.remove(), 300); + }, 3000); + + // Update the preview with the new banner data + if (result.image) { + currentImage = result.image; + document.getElementById('imagePreview').src = currentImage; + document.getElementById('imagePreviewContainer').style.display = 'block'; + document.getElementById('removeImageBtn').style.display = 'inline-block'; + } - alert('Banner byl úspěšně uložen'); updateBannerPreview(); } catch (error) { - console.error('Error saving banner:', error); - alert('Nepodařilo se uložit banner'); + console.error('Chyba při ukládání banneru:', error); + + // Show error message + const notification = document.createElement('div'); + notification.className = 'notification error'; + notification.innerHTML = ` ${error.message || 'Nepodařilo se uložit banner'}`; + document.body.appendChild(notification); + + // Remove notification after 5 seconds + setTimeout(() => { + notification.classList.add('fade-out'); + setTimeout(() => notification.remove(), 300); + }, 5000); + + } finally { + // Reset button state + saveBtn.disabled = false; + saveBtn.innerHTML = originalBtnText; } } @@ -447,19 +635,52 @@ // Update banner preview function updateBannerPreview() { - if (!bannerText.value.trim()) { + const bannerText = document.getElementById('bannerText').value; + const bannerMargin = document.getElementById('bannerMargin').value; + const bannerBorderRadius = document.getElementById('bannerBorderRadius').value; + const bannerFontSize = document.getElementById('bannerFontSize').value; + const bannerBgColor = document.getElementById('bannerBgColor').value; + const bannerTextColor = document.getElementById('bannerTextColor').value; + const bannerTextAlign = document.getElementById('bannerTextAlign').value; + const bannerPadding = document.getElementById('bannerPadding').value; + + const bannerPreview = document.getElementById('bannerPreview'); + const bannerPreviewText = bannerPreview.querySelector('.banner-preview-text'); + const bannerPreviewBg = bannerPreview.querySelector('.banner-preview-bg'); + const bannerPreviewContent = bannerPreview.querySelector('.banner-preview-content'); + + if (!bannerText.trim() && !currentImage) { bannerPreview.style.display = 'none'; return; } - bannerPreview.style.display = 'block'; - bannerPreview.textContent = bannerText.value; - bannerPreview.style.backgroundColor = bannerBgColor.value; - bannerPreview.style.color = bannerTextColor.value; - bannerPreview.style.textAlign = bannerTextAlign.value; - bannerPreview.style.fontSize = `${bannerFontSize.value}px`; - bannerPreview.style.padding = `${bannerPadding.value}px`; + bannerPreview.style.margin = `${bannerMargin}px auto`; + bannerPreview.style.borderRadius = `${bannerBorderRadius}px`; + + // Update banner content + bannerPreviewText.textContent = bannerText || ''; + bannerPreviewText.style.fontSize = `${bannerFontSize}px`; + + // Update background and text colors + bannerPreview.style.backgroundColor = bannerBgColor; + bannerPreviewText.style.color = bannerTextColor; + + // Update text alignment + bannerPreview.style.textAlign = bannerTextAlign; + + // Update padding + bannerPreviewContent.style.padding = `${bannerPadding}px`; + + // Handle image + if (currentImage) { + bannerPreview.classList.add('with-image'); + bannerPreviewBg.style.backgroundImage = `url(${currentImage})`; + bannerPreviewBg.style.display = 'block'; + } else { + bannerPreview.classList.remove('with-image'); + bannerPreviewBg.style.display = 'none'; + } } // Apply preset diff --git a/auth.go b/auth.go index 26a9ebf..0e719d6 100644 --- a/auth.go +++ b/auth.go @@ -29,7 +29,7 @@ var ( adminUsername = "admin" // In a real app, store hashed password and retrieve from a secure storage - adminPasswordHash = mustHashPassword("admin123") // Default password, should be changed after first login + adminPasswordHash = mustHashPassword("admin") // Default password, should be changed after first login ) func getJWTKey() []byte { diff --git a/banner.go b/banner.go index a9cee11..1a9d996 100644 --- a/banner.go +++ b/banner.go @@ -2,82 +2,121 @@ package main import ( "encoding/json" + "io" + "io/ioutil" "log" "net/http" "os" + "path/filepath" + "strings" "sync" ) +// Initialize banner data +func init() { + // Create data directory if it doesn't exist + if err := os.MkdirAll("data", 0755); err != nil { + log.Printf("Warning: Failed to create data directory: %v", err) + } + + // Create uploads directory if it doesn't exist + if err := os.MkdirAll(uploadDir, 0755); err != nil { + log.Printf("Warning: Failed to create uploads directory: %v", err) + } + + // Load banner data from file if it exists + if data, err := ioutil.ReadFile(bannerDataFile); err == nil { + if err := json.Unmarshal(data, &banner); err != nil { + log.Printf("Error loading banner data: %v", err) + initDefaultBanner() + } + } else { + initDefaultBanner() + } +} + +const ( + bannerDataFile = "data/banner.json" + uploadDir = "uploads" +) + +// Ensure directories exist +func ensureDirs() error { + if err := os.MkdirAll(filepath.Dir(bannerDataFile), 0755); err != nil { + return err + } + if err := os.MkdirAll(uploadDir, 0755); err != nil { + return err + } + return nil +} + +type BannerContent struct { + Text string `json:"text"` + Image string `json:"image,omitempty"` + Link string `json:"link,omitempty"` + Style BannerStyle `json:"style"` +} + type BannerStyle struct { BackgroundColor string `json:"backgroundColor"` TextColor string `json:"textColor"` - FontSize string `json:"fontSize"` TextAlign string `json:"textAlign"` + FontSize string `json:"fontSize"` Padding string `json:"padding"` Margin string `json:"margin"` BorderRadius string `json:"borderRadius"` IsVisible bool `json:"isVisible"` } -type BannerContent struct { - Text string `json:"text"` - Image string `json:"image,omitempty"` - Link string `json:"link,omitempty"` - Style BannerStyle `json:"style"` -} - var ( banner BannerContent bannerLock sync.RWMutex - bannerFile = "banner.json" ) func init() { - // Initialize with default values + // Ensure directories exist + if err := ensureDirs(); err != nil { + log.Printf("Warning: Failed to create required directories: %v", err) + } + + // Load banner data from file if it exists + if data, err := ioutil.ReadFile(bannerDataFile); err == nil { + if err := json.Unmarshal(data, &banner); err != nil { + log.Printf("Error loading banner data: %v", err) + initDefaultBanner() + } + } else { + initDefaultBanner() + } +} + +func initDefaultBanner() { banner = BannerContent{ - Text: "Důležité oznámení: Tento banner lze upravit v administraci.", + Text: "Vítejte na našem webu!", Style: BannerStyle{ BackgroundColor: "#f8d7da", TextColor: "#721c24", - FontSize: "16px", TextAlign: "center", - Padding: "10px", + FontSize: "18px", + Padding: "20px", + Margin: "20px", + BorderRadius: "8px", IsVisible: true, }, } - loadBanner() + saveBannerData() } -func loadBanner() { - if _, err := os.Stat(bannerFile); os.IsNotExist(err) { - saveBanner() - return - } - - data, err := os.ReadFile(bannerFile) - if err != nil { - log.Printf("Error reading banner file: %v", err) - return - } - +func saveBannerData() error { bannerLock.Lock() defer bannerLock.Unlock() - if err := json.Unmarshal(data, &banner); err != nil { - log.Printf("Error parsing banner data: %v", err) - } -} - -func saveBanner() error { - bannerLock.RLock() - defer bannerLock.RUnlock() - data, err := json.MarshalIndent(banner, "", " ") if err != nil { return err } - - return os.WriteFile(bannerFile, data, 0644) + return ioutil.WriteFile(bannerDataFile, data, 0644) } func GetBannerHandler(w http.ResponseWriter, r *http.Request) { @@ -85,31 +124,105 @@ func GetBannerHandler(w http.ResponseWriter, r *http.Request) { defer bannerLock.RUnlock() w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(banner) } func UpdateBannerHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - var newBanner BannerContent - if err := json.NewDecoder(r.Body).Decode(&newBanner); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + + // Parse multipart form for file uploads + if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max + http.Error(w, "Error parsing form data", http.StatusBadRequest) return } + newBanner := BannerContent{ + Text: r.FormValue("text"), + Link: r.FormValue("link"), + Style: BannerStyle{ + BackgroundColor: r.FormValue("style[backgroundColor]"), + TextColor: r.FormValue("style[textColor]"), + TextAlign: r.FormValue("style[textAlign]"), + FontSize: r.FormValue("style[fontSize]"), + Padding: r.FormValue("style[padding]"), + Margin: r.FormValue("style[margin]"), + BorderRadius: r.FormValue("style[borderRadius]"), + IsVisible: r.FormValue("style[isVisible]") == "true", + }, + } + + // Handle file upload + file, handler, err := r.FormFile("image") + if err == nil { + defer file.Close() + + // Ensure uploads directory exists + if err := ensureDirs(); err != nil { + http.Error(w, "Error preparing upload directory", http.StatusInternalServerError) + return + } + + // Create a new file in the uploads directory with a unique name + ext := filepath.Ext(handler.Filename) + tempFile, err := ioutil.TempFile(uploadDir, "upload-*"+ext) + if err != nil { + http.Error(w, "Error creating file", http.StatusInternalServerError) + return + } + defer tempFile.Close() + + // Copy the uploaded file to the destination file + if _, err := io.Copy(tempFile, file); err != nil { + http.Error(w, "Error saving file", http.StatusInternalServerError) + return + } + + // Update banner data with the new image path + newBanner.Image = "/uploads/" + filepath.Base(tempFile.Name()) + } else if r.FormValue("removeImage") == "true" { + // If removeImage is set, clear the image + newBanner.Image = "" + } else { + // Keep the existing image if no new one is uploaded + bannerLock.RLock() + newBanner.Image = banner.Image + bannerLock.RUnlock() + } + + // Update banner data bannerLock.Lock() banner = newBanner - err := saveBanner() + err = saveBannerData() bannerLock.Unlock() if err != nil { - http.Error(w, "Error saving banner", http.StatusInternalServerError) + http.Error(w, "Error saving banner data", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + json.NewEncoder(w).Encode(banner) +} + +// ServeUploads handles serving uploaded files +func ServeUploads(w http.ResponseWriter, r *http.Request) { + // Only serve files from the uploads directory + if !strings.HasPrefix(r.URL.Path, "/"+uploadDir+"/") { + http.NotFound(w, r) + return + } + // Strip the leading slash to get the relative path + http.ServeFile(w, r, r.URL.Path[1:]) } diff --git a/main.go b/main.go index dcd627d..5beb31a 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,14 @@ type GeoCoords struct { func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) + // Create necessary directories + if err := os.MkdirAll("data", 0755); err != nil { + log.Fatalf("Failed to create data directory: %v", err) + } + if err := os.MkdirAll("uploads", 0755); err != nil { + log.Fatalf("Failed to create uploads directory: %v", err) + } + r := mux.NewRouter() // Set up reverse proxy to kontakt service @@ -48,6 +56,7 @@ func main() { // Public routes r.PathPrefix("/kontakt/").Handler(http.StripPrefix("/kontakt", kontaktProxy)) + r.PathPrefix("/uploads/").Handler(http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads")))) r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) @@ -60,10 +69,26 @@ func main() { api := r.PathPrefix("/api").Subrouter() api.Use(AuthMiddleware) api.HandleFunc("/submit", handleSubmit).Methods("POST") - api.HandleFunc("/banner/update", UpdateBannerHandler).Methods("POST") + api.HandleFunc("/banner/update", UpdateBannerHandler).Methods("POST", "OPTIONS") // Public banner endpoint - r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET") + r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET", "OPTIONS") + + // Add CORS middleware for API + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) + }) // Admin routes r.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {