diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index cc92a7c..a9c1e17 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -19,6 +19,38 @@ The application supports the following environment variables: | `KVK` | `12345678` | KVK number for contact information | | `EMAIL` | `info@hogelandlinux.nl` | Contact email address | | `PHONE` | `+31 6 12345678` | Contact phone number | +| `TELEGRAM_BOT_TOKEN` | *(empty)* | Telegram bot token for contact form notifications | +| `TELEGRAM_CHAT_ID` | *(empty)* | Telegram chat ID where notifications will be sent | + +## Telegram Integration Setup (Optional) + +The contact form can send notifications to a Telegram chat. To set this up: + +### 1. Create a Telegram Bot + +1. Message [@BotFather](https://t.me/BotFather) on Telegram +2. Send `/newbot` command +3. Follow the instructions to create your bot +4. Copy the bot token (looks like `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`) + +### 2. Get Your Chat ID + +1. Add your bot to the chat where you want notifications +2. Send a message to the bot in that chat +3. Visit `https://api.telegram.org/bot/getUpdates` +4. Look for the `chat.id` field in the response +5. Copy the chat ID (can be positive or negative number) + +### 3. Configure Environment Variables + +Set the following environment variables in your deployment: + +```bash +TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz +TELEGRAM_CHAT_ID=123456789 +``` + +**Note:** If these variables are not set, the contact form will still work but won't send Telegram notifications. ## Coolify Deployment Steps @@ -52,8 +84,12 @@ COMPANY_NAME=Hogeland Linux KVK=12345678 EMAIL=info@hogelandlinux.nl PHONE=+31 6 12345678 +TELEGRAM_BOT_TOKEN=your_bot_token_here +TELEGRAM_CHAT_ID=your_chat_id_here ``` +**Note:** The Telegram variables are optional. If not set, the contact form will work but won't send notifications. + ### 5. Configure Domain - Set your desired domain/subdomain diff --git a/README.md b/README.md index 3d85cc3..e47ffbc 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Phone: "+31 6 12345678", // Uw telefoonnummer ## Pagina's ### Hoofdpagina (/) -- Hero sectie met animeerde terminal demo +- Hero sectie met duidelijke call-to-action - Voordelen van Linux migratie - Informatie over Windows 10 End of Life - Linux distributies showcase met aanbevelingen @@ -81,12 +81,12 @@ Het design is: - **Responsive**: Werkt op desktop, tablet en mobiel - **Professioneel**: Vertrouwen opwekkend voor bedrijven - **Eco-vriendelijk**: Groene kleurenpalet die duurzaamheid benadrukt -- **Tech-georiรซnteerd**: Linux terminal demo en visuele mockups +- **Tech-georiรซnteerd**: Visuele mockups en functionaliteit demonstraties ## Technische Features ### Visual Elements -- **Animeerde Terminal**: Realistische Arch Linux terminal met pacman en fastfetch +- **Clean Design**: Moderne en professionele interface zonder technische complexiteit - **Distro Showcase**: 6 populaire Linux distributies met doelgroepen - **Desktop Mockups**: Visuele representaties van Linux interfaces - **Performance Vergelijkingen**: Grafische weergave van Linux vs Windows prestaties diff --git a/docker-compose.yml b/docker-compose.yml index 2e29690..d6fb8bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,6 @@ services: - KVK=12345678 - EMAIL=info@hogelandlinux.nl - PHONE=+31 6 12345678 + - TELEGRAM_BOT_TOKEN= # Set your Telegram bot token here + - TELEGRAM_CHAT_ID= # Set your Telegram chat ID here restart: unless-stopped \ No newline at end of file diff --git a/main.go b/main.go index dd9ce99..e829b6e 100644 --- a/main.go +++ b/main.go @@ -1,28 +1,48 @@ package main import ( + "bytes" + "encoding/json" + "fmt" "html/template" + "io" "log" "net/http" "os" + "strings" + "sync" + "time" ) // Configuration holds all website configuration type Config struct { - CompanyName string - KVK string - Email string - Phone string - Port string + CompanyName string + KVK string + Email string + Phone string + Port string + TelegramBotToken string + TelegramChatID string } // PageData holds data for template rendering type PageData struct { - CompanyName string - Title string - KVK string - Email string - Phone string + Config + Title string + CurrentYear int + ErrorMessage string + SuccessMessage string + FormData ContactForm +} + +// ContactForm holds form data +type ContactForm struct { + Name string + Email string + Phone string + Computer string + Service string + Message string } // Server holds the application state @@ -31,6 +51,13 @@ type Server struct { templates *template.Template } +// Rate limiting +var ( + mu sync.Mutex + lastSubmissionTime = make(map[string]time.Time) + submissionCooldown = 5 * time.Minute +) + // getEnv returns environment variable or default value func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { @@ -43,11 +70,13 @@ func getEnv(key, defaultValue string) string { func NewServer() *Server { // Configuration - can be overridden with environment variables config := Config{ - CompanyName: getEnv("COMPANY_NAME", "Hogeland Linux"), - KVK: getEnv("KVK", "12345678"), // Replace with actual KVK number - Email: getEnv("EMAIL", "info@hogelandlinux.nl"), - Phone: getEnv("PHONE", "+31 6 12345678"), - Port: ":" + getEnv("PORT", "8080"), + CompanyName: getEnv("COMPANY_NAME", "Hogeland Linux"), + KVK: getEnv("KVK", "12345678"), // Replace with actual KVK number + Email: getEnv("EMAIL", "info@hogelandlinux.nl"), + Phone: getEnv("PHONE", "+31 6 12345678"), + Port: ":" + getEnv("PORT", "8080"), + TelegramBotToken: getEnv("TELEGRAM_BOT_TOKEN", ""), // Set this in environment + TelegramChatID: getEnv("TELEGRAM_CHAT_ID", ""), // Set this in environment } // Parse templates with error handling @@ -65,11 +94,9 @@ func NewServer() *Server { // createPageData creates PageData with the given title func (s *Server) createPageData(title string) PageData { return PageData{ - CompanyName: s.config.CompanyName, + Config: s.config, Title: title, - KVK: s.config.KVK, - Email: s.config.Email, - Phone: s.config.Phone, + CurrentYear: time.Now().Year(), } } @@ -90,9 +117,183 @@ func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) { // contactHandler handles the contact page func (s *Server) contactHandler(w http.ResponseWriter, r *http.Request) { data := s.createPageData("Contact - " + s.config.CompanyName) + + if r.Method == "POST" { + s.handleContactForm(w, r, &data) + return + } + s.renderTemplate(w, "contact.html", data) } +// handleContactForm processes the contact form submission +func (s *Server) handleContactForm(w http.ResponseWriter, r *http.Request, data *PageData) { + // Get client IP for rate limiting + ip := getClientIP(r) + + // Check rate limiting + mu.Lock() + if lastTime, exists := lastSubmissionTime[ip]; exists { + if time.Since(lastTime) < submissionCooldown { + mu.Unlock() + data.ErrorMessage = "U heeft recent al een bericht verstuurd. Probeer het over een paar minuten opnieuw." + s.renderTemplate(w, "contact.html", *data) + return + } + } + mu.Unlock() + + // Parse form data + err := r.ParseForm() + if err != nil { + log.Printf("Error parsing form: %v", err) + data.ErrorMessage = "Er is een fout opgetreden bij het verwerken van uw bericht." + s.renderTemplate(w, "contact.html", *data) + return + } + + // Extract form data + form := ContactForm{ + Name: strings.TrimSpace(r.FormValue("name")), + Email: strings.TrimSpace(r.FormValue("email")), + Phone: strings.TrimSpace(r.FormValue("phone")), + Computer: strings.TrimSpace(r.FormValue("computer")), + Service: strings.TrimSpace(r.FormValue("service")), + Message: strings.TrimSpace(r.FormValue("message")), + } + + // Store form data for re-rendering on error + data.FormData = form + + // Validate required fields + if form.Name == "" || form.Email == "" || form.Message == "" { + data.ErrorMessage = "Vul alle verplichte velden in (naam, email, bericht)." + s.renderTemplate(w, "contact.html", *data) + return + } + + // Send to Telegram if configured + if s.config.TelegramBotToken != "" && s.config.TelegramChatID != "" { + err := s.sendToTelegram(form) + if err != nil { + log.Printf("Error sending message to Telegram: %v", err) + data.ErrorMessage = "Er is een fout opgetreden bij het versturen van uw bericht. Probeer het later opnieuw." + s.renderTemplate(w, "contact.html", *data) + return + } + } + + // Update last submission time on success + mu.Lock() + lastSubmissionTime[ip] = time.Now() + mu.Unlock() + + // On success, render success message + data.SuccessMessage = "Bedankt voor uw bericht! Wij nemen zo snel mogelijk contact met u op." + data.FormData = ContactForm{} // Clear form data + s.renderTemplate(w, "contact.html", *data) +} + +// sendToTelegram sends the contact form data to Telegram +func (s *Server) sendToTelegram(form ContactForm) error { + // Format message + message := fmt.Sprintf( + "๐Ÿ”” Nieuw contactformulier\n\n"+ + "๐Ÿ‘ค *Naam:* %s\n"+ + "๐Ÿ“ง *Email:* %s\n"+ + "๐Ÿ“ž *Telefoon:* %s\n"+ + "๐Ÿ’ป *Computer:* %s\n"+ + "๐Ÿ› ๏ธ *Service:* %s\n\n"+ + "๐Ÿ’ฌ *Bericht:*\n%s", + escapeMarkdown(form.Name), + escapeMarkdown(form.Email), + escapeMarkdown(form.Phone), + escapeMarkdown(form.Computer), + escapeMarkdown(form.Service), + escapeMarkdown(form.Message), + ) + + // Prepare Telegram API request + telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", s.config.TelegramBotToken) + + payload := map[string]interface{}{ + "chat_id": s.config.TelegramChatID, + "text": message, + "parse_mode": "Markdown", + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal JSON: %v", err) + } + + // Send HTTP request + resp, err := http.Post(telegramURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("failed to send HTTP request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Telegram API error: %s, Status Code: %d", string(bodyBytes), resp.StatusCode) + } + + return nil +} + +// escapeMarkdown escapes special characters for Telegram Markdown +func escapeMarkdown(text string) string { + if text == "" { + return "N/A" + } + // Escape special Markdown characters + replacer := strings.NewReplacer( + "*", "\\*", + "_", "\\_", + "`", "\\`", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "~", "\\~", + ">", "\\>", + "#", "\\#", + "+", "\\+", + "-", "\\-", + "=", "\\=", + "|", "\\|", + "{", "\\{", + "}", "\\}", + ".", "\\.", + "!", "\\!", + ) + return replacer.Replace(text) +} + +// getClientIP extracts the client IP address from the request +func getClientIP(r *http.Request) string { + // Check X-Forwarded-For header first (for reverse proxies) + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + // X-Forwarded-For can contain multiple IPs, take the first one + if idx := strings.Index(xff, ","); idx != -1 { + return strings.TrimSpace(xff[:idx]) + } + return strings.TrimSpace(xff) + } + + // Check X-Real-IP header + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return strings.TrimSpace(xri) + } + + // Fall back to RemoteAddr + if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + return r.RemoteAddr +} + // healthHandler provides a simple health check endpoint func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/static/style.css b/static/style.css index ec66231..a5412bc 100644 --- a/static/style.css +++ b/static/style.css @@ -146,10 +146,10 @@ p { max-width: 1200px; margin: 0 auto; padding: 0 20px; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4rem; + display: flex; + flex-direction: column; align-items: center; + text-align: center; } .hero-content h2 { @@ -169,100 +169,6 @@ p { gap: 1rem; } -.hero-image { - display: flex; - justify-content: center; - align-items: center; -} - -/* Terminal Window Styling */ -.terminal-window { - background: #1a1a1a; - border-radius: 12px; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); - max-width: 520px; - margin: 0 auto 0 0; - overflow: hidden; - border: 1px solid #333; -} - -.terminal-header { - background: #2d2d2d; - padding: 12px 16px; - display: flex; - align-items: center; - gap: 1rem; - border-bottom: 1px solid #333; -} - -.terminal-buttons { - display: flex; - gap: 8px; -} - -.terminal-buttons span { - width: 12px; - height: 12px; - border-radius: 50%; -} - -.btn-close { - background: #ff5f57; -} - -.btn-minimize { - background: #ffbd2e; -} - -.btn-maximize { - background: #28ca42; -} - -.terminal-title { - color: #ccc; - font-size: 0.9rem; - font-weight: 500; -} - -.terminal-body { - padding: 20px; - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Source Code Pro', monospace; - font-size: 14px; - line-height: 1.6; - min-height: 200px; -} - -.terminal-line { - margin-bottom: 8px; - display: flex; - align-items: center; - gap: 8px; -} - -.prompt { - color: #10b981; - font-weight: 600; - min-width: 180px; -} - -.command { - color: #60a5fa; -} - -.output { - color: #d1d5db; -} - -.cursor { - color: #10b981; - animation: blink 1s infinite; -} - -@keyframes blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } -} - /* Buttons */ .btn { padding: 12px 24px; @@ -872,6 +778,33 @@ p { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); } +/* Alert Styles */ +.alert { + padding: 1rem 1.5rem; + border-radius: 8px; + margin-bottom: 1.5rem; + border: 1px solid; + font-size: 1rem; + line-height: 1.5; +} + +.alert-error { + background-color: #fef2f2; + color: #dc2626; + border-color: #fecaca; +} + +.alert-success { + background-color: #f0fdf4; + color: #16a34a; + border-color: #bbf7d0; +} + +.alert strong { + font-weight: 600; + margin-right: 0.5rem; +} + .form-group { margin-bottom: 1.5rem; } @@ -1015,12 +948,6 @@ footer { padding: 3rem 0; } - .hero-content { - grid-template-columns: 1fr; - gap: 2rem; - text-align: center; - } - .hero-content h2 { font-size: 2.5rem; line-height: 1.2; @@ -1036,19 +963,6 @@ footer { gap: 1rem; } - .terminal-window { - max-width: 100%; - margin: 0; - border-radius: 8px; - } - - .terminal-body { - padding: 16px; - font-size: 13px; - min-height: 180px; - overflow-x: auto; - } - .benefits-grid { grid-template-columns: 1fr; gap: 1.5rem; @@ -1154,22 +1068,6 @@ footer { max-width: 300px; } - .terminal-window { - border-radius: 6px; - overflow: hidden; - } - - .terminal-body { - padding: 12px; - font-size: 12px; - min-height: 160px; - } - - .prompt { - min-width: 120px; - font-size: 11px; - } - /* Card padding adjustments */ .benefit-card, .service-card, .distro-card, .cta-benefit { padding: 1.25rem; @@ -1266,23 +1164,12 @@ footer { padding: 2rem 0; } - .hero-content { - grid-template-columns: 1fr 1fr; - gap: 2rem; - text-align: left; - } - .hero-content h2 { font-size: 2rem; } .hero-buttons { - justify-content: flex-start; - } - - .terminal-window { - max-height: 250px; - overflow-y: auto; + justify-content: center; } } @@ -1296,11 +1183,6 @@ footer { font-size: 1rem; } - .terminal-body { - font-size: 11px; - padding: 10px; - } - .benefit-card, .service-card, .distro-card, .cta-benefit { padding: 1rem; } @@ -1313,9 +1195,7 @@ footer { /* High DPI screens */ @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - .terminal-body { - font-size: 13px; - } + /* No terminal-specific styles needed */ } /* Animations */ diff --git a/templates/contact.html b/templates/contact.html index f12f479..a0cd2e2 100644 --- a/templates/contact.html +++ b/templates/contact.html @@ -79,42 +79,55 @@

Stuur een bericht

-
+ + {{if .ErrorMessage}} +
+ โŒ Fout: {{.ErrorMessage}} +
+ {{end}} + + {{if .SuccessMessage}} +
+ โœ… Gelukt: {{.SuccessMessage}} +
+ {{end}} + +
- +
- +
- +
- +
- +
@@ -172,7 +185,7 @@
diff --git a/templates/index.html b/templates/index.html index eed986d..672ad3a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,22 +12,24 @@ @@ -43,47 +45,6 @@ Gratis advies -
@@ -166,7 +127,9 @@
- +

Ubuntu

Populair
@@ -181,7 +144,11 @@
- +

Linux Mint

Windows-achtig
@@ -196,7 +163,9 @@
- +

Pop!_OS

Gaming
@@ -211,7 +180,9 @@
- +

Elementary OS

Mooi
@@ -226,7 +197,9 @@
- +

Fedora

Geavanceerd
@@ -241,7 +214,11 @@
- +

Garuda Linux

Prestaties
@@ -398,7 +375,7 @@
@@ -426,65 +403,7 @@ }); }); - // Terminal animation - const terminalBody = document.querySelector('.terminal-body'); - const lines = [ - { type: 'prompt', content: 'ainrommer@computer ~ $' }, - { type: 'command', content: 'neofetch', delay: 1000 }, - { type: 'output', content: '\n .-/+oossssoo+/-.\n .:+ssssssssssssssss+:.\n -+ssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n -+ssssssssssssssssssssss+-\n-+ssssssssssssssssssssss+-\n`-+ssssssssssssssssssss+-`\n `-+ssssssssssssssssss+-`\n `-+ssssssssssssssss+-`\n `-+ssssssssssssss+-`\n `-+ssssssssssss+-`\n `-+ssssssssss+-`\n `-+ssssssss+-`\n `-+ssssss+-`\n `-+ssss+-`\n `-+ss+-`\n `-+s+-`\n `-+-`\n ``\nOS: Arch Linux x86_64\nKernel: 6.6.8-arch1-1\nUptime: 3 hours, 42 mins\nPackages: 1337 (pacman)\nShell: zsh 5.9\nResolution: 1920x1080\nDE: KDE Plasma 5.27.10\nWM: KWin\nWM Theme: Breeze\nTheme: Breeze Dark [Plasma], Breeze [GTK2/3]\nIcons: Breeze Dark [Plasma], breeze-dark [GTK2/3]\nTerminal: konsole\nCPU: AMD Ryzen 7 5800X (16) @ 3.800GHz\nGPU: NVIDIA GeForce RTX 3070\nMemory: 2847MiB / 32768MiB', delay: 2000 }, - { type: 'prompt', content: 'ainrommer@computer ~ $' }, - { type: 'command', content: 'sudo pacman -Syu', delay: 3000 }, - { type: 'output', content: ':: Synchronizing package databases...\n core 174.7 KiB 623 KiB/s 00:00 [######################] 100%\n extra 1744.4 KiB 1234 KiB/s 00:01 [######################] 100%\n multilib 193.9 KiB 567 KiB/s 00:00 [######################] 100%\n:: Starting full system upgrade...\n:: Replace gtk4 with extra/gtk4? [Y/n] y\nthere is nothing to do', delay: 4000 }, - { type: 'prompt', content: 'ainrommer@computer ~ $' }, - { type: 'cursor', content: '', delay: 5000 } - ]; - - function typeWriter(text, element, speed = 50) { - return new Promise((resolve) => { - let i = 0; - const timer = setInterval(() => { - if (i < text.length) { - element.textContent += text.charAt(i); - i++; - } else { - clearInterval(timer); - resolve(); - } - }, speed); - }); - } - - function animateTerminal() { - terminalBody.innerHTML = ''; - let currentDelay = 0; - - lines.forEach((line, index) => { - setTimeout(() => { - const lineElement = document.createElement('div'); - lineElement.className = 'terminal-line'; - - if (line.type === 'prompt') { - lineElement.innerHTML = '' + line.content + ''; - } else if (line.type === 'command') { - lineElement.innerHTML = 'ainrommer@computer ~ $ ' + line.content + ''; - } else if (line.type === 'output') { - lineElement.innerHTML = '' + line.content + ''; - } else if (line.type === 'cursor') { - lineElement.innerHTML = 'ainrommer@computer ~ $ โ–ˆ'; - } - - terminalBody.appendChild(lineElement); - terminalBody.scrollTop = terminalBody.scrollHeight; - }, currentDelay); - - currentDelay += line.delay || 0; - }); - } - - // Start animation when page loads - window.addEventListener('load', () => { - setTimeout(animateTerminal, 1000); - }); + // Mobile menu functionality and other interactive features \ No newline at end of file