Správa banneru
@@ -1302,6 +1360,169 @@ function handleImageUpload(event) {
reader.readAsDataURL(file);
}
+// App Management Functions
+async function loadApps() {
+ try {
+ const response = await fetch('/api/apps');
+ if (!response.ok) throw new Error('Nepodařilo se načíst seznam aplikací');
+
+ const apps = await response.json();
+ const appsList = document.getElementById('appsList');
+
+ if (apps.length === 0) {
+ appsList.innerHTML = '
Žádné aplikace nebyly nalezeny.
';
+ return;
+ }
+
+ appsList.innerHTML = apps.map(app => `
+
+
+ ${app.icon ?
+ `

` :
+ `
+
+
`
+ }
+
+
${app.name}
+
${app.url}
+
+
+
+
+
+
+
+ `).join('');
+
+ // Add event listeners to buttons
+ document.querySelectorAll('.edit-app-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const appId = btn.dataset.appId;
+ editApp(appId);
+ });
+ });
+
+ document.querySelectorAll('.delete-app-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const appId = btn.dataset.appId;
+ if (confirm('Opravdu chcete tuto aplikaci smazat?')) {
+ deleteApp(appId);
+ }
+ });
+ });
+
+ } catch (error) {
+ console.error('Chyba při načítání aplikací:', error);
+ showNotification('Nepodařilo se načíst seznam aplikací', 'error');
+ }
+}
+
+async function saveApp(event) {
+ event.preventDefault();
+
+ const form = event.target;
+ const formData = new FormData();
+ const appId = document.getElementById('appId').value;
+
+ formData.append('name', document.getElementById('appName').value);
+ formData.append('url', document.getElementById('appUrl').value);
+ formData.append('description', document.getElementById('appDescription').value);
+
+ const iconInput = document.getElementById('appIcon');
+ if (iconInput.files.length > 0) {
+ formData.append('icon', iconInput.files[0]);
+ }
+
+ try {
+ const url = appId ? `/api/apps/${appId}` : '/api/apps';
+ const method = appId ? 'PUT' : 'POST';
+
+ const response = await fetch(url, {
+ method,
+ body: formData,
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || 'Nepodařilo se uložit aplikaci');
+ }
+
+ closeAppModal();
+ await loadApps();
+ showNotification('Aplikace byla úspěšně uložena', 'success');
+
+ } catch (error) {
+ console.error('Chyba při ukládání aplikace:', error);
+ showNotification(error.message || 'Nepodařilo se uložit aplikaci', 'error');
+ }
+}
+
+async function editApp(appId) {
+ try {
+ const response = await fetch(`/api/apps/${appId}`);
+ if (!response.ok) throw new Error('Nepodařilo se načíst data aplikace');
+
+ const app = await response.json();
+
+ document.getElementById('appId').value = app.id;
+ document.getElementById('appName').value = app.name;
+ document.getElementById('appUrl').value = app.url;
+ document.getElementById('appDescription').value = app.description || '';
+ document.getElementById('appModalTitle').textContent = 'Upravit aplikaci';
+
+ document.getElementById('appModal').classList.remove('hidden');
+
+ } catch (error) {
+ console.error('Chyba při načítání aplikace:', error);
+ showNotification('Nepodařilo se načíst data aplikace', 'error');
+ }
+}
+
+async function deleteApp(appId) {
+ try {
+ const response = await fetch(`/api/apps/${appId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || 'Nepodařilo se smazat aplikaci');
+ }
+
+ await loadApps();
+ showNotification('Aplikace byla úspěšně smazána', 'success');
+
+ } catch (error) {
+ console.error('Chyba při mazání aplikace:', error);
+ showNotification(error.message || 'Nepodařilo se smazat aplikaci', 'error');
+ }
+}
+
+function openAddAppModal() {
+ document.getElementById('appForm').reset();
+ document.getElementById('appId').value = '';
+ document.getElementById('appModalTitle').textContent = 'Přidat aplikaci';
+ document.getElementById('appModal').classList.remove('hidden');
+}
+
+function closeAppModal() {
+ document.getElementById('appModal').classList.add('hidden');
+}
+
// Logout functionality
document.getElementById('logoutBtn').addEventListener('click', function() {
localStorage.removeItem('token');
@@ -2241,6 +2462,20 @@ function debounce(func, wait) {
};
}
+// App management event listeners
+document.getElementById('addAppBtn').addEventListener('click', openAddAppModal);
+document.getElementById('closeAppModal').addEventListener('click', closeAppModal);
+document.getElementById('cancelAppBtn').addEventListener('click', closeAppModal);
+document.getElementById('appForm').addEventListener('submit', saveApp);
+
+// Close modal when clicking outside
+const appModal = document.getElementById('appModal');
+appModal.addEventListener('click', (e) => {
+ if (e.target === appModal) {
+ closeAppModal();
+ }
+});
+
// These event listeners will be set up after the DOM is fully loaded
// Setup draggable image functionality
diff --git a/main.go b/main.go
index 3b3f2d4..9974721 100644
--- a/main.go
+++ b/main.go
@@ -11,6 +11,7 @@ import (
"net/url"
"os"
"os/exec"
+ "path/filepath"
"strings"
"time"
@@ -18,6 +19,16 @@ import (
"gopkg.in/gomail.v2"
)
+type App struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ URL string `json:"url"`
+ Description string `json:"description,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
type TripEntry struct {
Name string `json:"name"`
Vehicle string `json:"vehicle"`
@@ -70,6 +81,13 @@ func main() {
api.Use(AuthMiddleware)
api.HandleFunc("/submit", handleSubmit).Methods("POST")
api.HandleFunc("/banner/update", UpdateBannerHandler).Methods("POST", "OPTIONS")
+
+ // App management routes
+ api.HandleFunc("/apps", GetAppsHandler).Methods("GET")
+ api.HandleFunc("/apps", CreateAppHandler).Methods("POST")
+ api.HandleFunc("/apps/{id}", GetAppHandler).Methods("GET")
+ api.HandleFunc("/apps/{id}", UpdateAppHandler).Methods("PUT")
+ api.HandleFunc("/apps/{id}", DeleteAppHandler).Methods("DELETE")
// Public endpoints
r.HandleFunc("/api/banner", GetBannerHandler).Methods("GET", "OPTIONS")
@@ -170,6 +188,196 @@ func enableCORS(next http.Handler) http.Handler {
})
}
+// App Handlers
+func GetAppsHandler(w http.ResponseWriter, r *http.Request) {
+ // In a real app, this would fetch from a database
+ apps := []App{
+ {
+ ID: "1",
+ Name: "Kontakt",
+ URL: "/kontakt",
+ Description: "Kontaktní formulář",
+ Icon: "",
+ CreatedAt: time.Now().Format(time.RFC3339),
+ UpdatedAt: time.Now().Format(time.RFC3339),
+ },
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(apps)
+}
+
+func GetAppHandler(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ // In a real app, this would fetch from a database
+ if id != "1" {
+ http.Error(w, "App not found", http.StatusNotFound)
+ return
+ }
+
+ app := App{
+ ID: id,
+ Name: "Kontakt",
+ URL: "/kontakt",
+ Description: "Kontaktní formulář",
+ Icon: "",
+ CreatedAt: time.Now().Format(time.RFC3339),
+ UpdatedAt: time.Now().Format(time.RFC3339),
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(app)
+}
+
+func CreateAppHandler(w http.ResponseWriter, r *http.Request) {
+ // Parse form data
+ err := r.ParseMultipartForm(10 << 20) // 10 MB max file size
+ if err != nil {
+ http.Error(w, "Error parsing form data", http.StatusBadRequest)
+ return
+ }
+
+ // Get form values
+ name := r.FormValue("name")
+ url := r.FormValue("url")
+ description := r.FormValue("description")
+
+ // Handle file upload
+ var iconPath string
+ file, handler, err := r.FormFile("icon")
+ if err == nil {
+ defer file.Close()
+
+ // Create uploads directory if it doesn't exist
+ if _, err := os.Stat("uploads"); os.IsNotExist(err) {
+ os.Mkdir("uploads", 0755)
+ }
+
+ // Generate a unique filename
+ ext := ""
+ if parts := strings.Split(handler.Filename, "."); len(parts) > 1 {
+ ext = "." + parts[len(parts)-1]
+ }
+ iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext)
+
+ // Create the file
+ f, err := os.Create(filepath.Join("uploads", iconPath))
+ if err != nil {
+ http.Error(w, "Error saving file", http.StatusInternalServerError)
+ return
+ }
+ defer f.Close()
+
+ // Copy the uploaded file to the created file
+ _, err = io.Copy(f, file)
+ if err != nil {
+ http.Error(w, "Error saving file", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ // In a real app, this would save to a database
+ app := App{
+ ID: fmt.Sprintf("%d", time.Now().UnixNano()),
+ Name: name,
+ URL: url,
+ Description: description,
+ Icon: iconPath,
+ CreatedAt: time.Now().Format(time.RFC3339),
+ UpdatedAt: time.Now().Format(time.RFC3339),
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(app)
+}
+
+func UpdateAppHandler(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ // In a real app, this would update in a database
+ if id != "1" {
+ http.Error(w, "App not found", http.StatusNotFound)
+ return
+ }
+
+ // Parse form data
+ err := r.ParseMultipartForm(10 << 20) // 10 MB max file size
+ if err != nil {
+ http.Error(w, "Error parsing form data", http.StatusBadRequest)
+ return
+ }
+
+ // Get form values
+ name := r.FormValue("name")
+ url := r.FormValue("url")
+ description := r.FormValue("description")
+
+ // Handle file upload if a new file is provided
+ var iconPath string
+ file, handler, err := r.FormFile("icon")
+ if err == nil {
+ defer file.Close()
+
+ // Create uploads directory if it doesn't exist
+ if _, err := os.Stat("uploads"); os.IsNotExist(err) {
+ os.Mkdir("uploads", 0755)
+ }
+
+ // Generate a unique filename
+ ext := ""
+ if parts := strings.Split(handler.Filename, "."); len(parts) > 1 {
+ ext = "." + parts[len(parts)-1]
+ }
+ iconPath = fmt.Sprintf("icon_%d%s", time.Now().UnixNano(), ext)
+
+ // Create the file
+ f, err := os.Create(filepath.Join("uploads", iconPath))
+ if err != nil {
+ http.Error(w, "Error saving file", http.StatusInternalServerError)
+ return
+ }
+ defer f.Close()
+
+ // Copy the uploaded file to the created file
+ _, err = io.Copy(f, file)
+ if err != nil {
+ http.Error(w, "Error saving file", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ // In a real app, this would update in a database
+ app := App{
+ ID: id,
+ Name: name,
+ URL: url,
+ Description: description,
+ Icon: iconPath, // This would be updated only if a new file was uploaded
+ CreatedAt: time.Now().Format(time.RFC3339), // In a real app, this would be fetched from the database
+ UpdatedAt: time.Now().Format(time.RFC3339),
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(app)
+}
+
+func DeleteAppHandler(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ // In a real app, this would delete from a database
+ if id != "1" {
+ http.Error(w, "App not found", http.StatusNotFound)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
func handleSubmit(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")