feat: added lots of work to landing page
This commit is contained in:
@@ -19,6 +19,7 @@ type Config struct {
|
||||
SMTPPort string
|
||||
SMTPUser string
|
||||
SMTPPass string
|
||||
AdminEmail string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
@@ -36,6 +37,7 @@ func LoadConfig() (*Config, error) {
|
||||
SMTPPort: getEnv("SMTP_PORT", ""),
|
||||
SMTPUser: getEnv("SMTP_USER", ""),
|
||||
SMTPPass: getEnv("SMTP_PASSWORD", ""),
|
||||
AdminEmail: os.Getenv("ADMIN_EMAIL"),
|
||||
}
|
||||
|
||||
if cfg.SMTPHost == "" {
|
||||
|
||||
@@ -58,6 +58,14 @@ func (db *DB) InitDB(ctx context.Context) error {
|
||||
body TEXT NOT NULL,
|
||||
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS contact_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
@@ -155,6 +163,28 @@ func (db *DB) GetNewsletter(
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
func (db *DB) AddContactMessage(
|
||||
ctx context.Context,
|
||||
name, email, subject, message string,
|
||||
) error {
|
||||
query := `
|
||||
INSERT INTO contact_messages (name, email, subject, message, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`
|
||||
|
||||
_, err := db.pool.Exec(
|
||||
ctx,
|
||||
query,
|
||||
name,
|
||||
email,
|
||||
subject,
|
||||
message,
|
||||
time.Now(),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) Close(ctx context.Context) {
|
||||
db.pool.Close()
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package email
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"html"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"landing/internal/config"
|
||||
)
|
||||
@@ -23,13 +23,6 @@ func (s *Sender) SendConfirmationEmail(
|
||||
email string,
|
||||
unsubscribeLink string,
|
||||
) error {
|
||||
// Parse SMTP port from env
|
||||
port, err := strconv.Atoi(s.cfg.SMTPPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SMTP port '%s': %w", s.cfg.SMTPPort, err)
|
||||
}
|
||||
|
||||
// Build email message
|
||||
subject := "Thanks for subscribing!"
|
||||
htmlBody := fmt.Sprintf(`
|
||||
<html>
|
||||
@@ -41,88 +34,143 @@ func (s *Sender) SendConfirmationEmail(
|
||||
</html>
|
||||
`, unsubscribeLink)
|
||||
|
||||
return s.sendEmail(email, subject, htmlBody)
|
||||
}
|
||||
|
||||
func (s *Sender) SendContactConfirmation(email, name string) error {
|
||||
subject := "We received your message - RideAware"
|
||||
htmlBody := fmt.Sprintf(`
|
||||
<html>
|
||||
<body>
|
||||
<h2>Thank you for reaching out, %s!</h2>
|
||||
<p>We've received your message and will get back to you as soon as possible.</p>
|
||||
<p>In the meantime, feel free to check out more about RideAware on our website.</p>
|
||||
<p>Best regards,<br>The RideAware Team</p>
|
||||
</body>
|
||||
</html>
|
||||
`, html.EscapeString(name))
|
||||
|
||||
return s.sendEmail(email, subject, htmlBody)
|
||||
}
|
||||
|
||||
func (s *Sender) SendContactNotification(
|
||||
adminEmail, name, email, subject, message string,
|
||||
) error {
|
||||
emailSubject := fmt.Sprintf("New contact message from %s", name)
|
||||
htmlBody := fmt.Sprintf(`
|
||||
<html>
|
||||
<body>
|
||||
<h3>New Contact Message</h3>
|
||||
<p><strong>From:</strong> %s (%s)</p>
|
||||
<p><strong>Subject:</strong> %s</p>
|
||||
<h4>Message:</h4>
|
||||
<p>%s</p>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
html.EscapeString(name),
|
||||
html.EscapeString(email),
|
||||
html.EscapeString(subject),
|
||||
strings.ReplaceAll(html.EscapeString(message), "\n", "<br>"),
|
||||
)
|
||||
|
||||
return s.sendEmail(adminEmail, emailSubject, htmlBody)
|
||||
}
|
||||
|
||||
func (s *Sender) sendEmail(toEmail, subject, htmlBody string) error {
|
||||
port, err := strconv.Atoi(s.cfg.SMTPPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SMTP port '%s': %w", s.cfg.SMTPPort, err)
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(
|
||||
"From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=utf-8\r\n\r\n%s",
|
||||
s.cfg.SMTPUser,
|
||||
email,
|
||||
toEmail,
|
||||
subject,
|
||||
htmlBody,
|
||||
)
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, port)
|
||||
|
||||
// Port 465 uses direct SSL/TLS
|
||||
return s.sendEmailSSL(addr, toEmail, message)
|
||||
}
|
||||
|
||||
func (s *Sender) sendEmailSSL(addr, toEmail, message string) error {
|
||||
// Create TLS config
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: s.cfg.SMTPHost,
|
||||
}
|
||||
|
||||
// Send email using smtp.SendMail
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, port)
|
||||
auth := smtp.PlainAuth("", s.cfg.SMTPUser, s.cfg.SMTPPass, s.cfg.SMTPHost)
|
||||
|
||||
// Use a custom dialer with timeout
|
||||
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||
// Try to dial with TLS
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||
return fmt.Errorf("failed to dial TLS to %s: %w", addr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Create SMTP client
|
||||
client, err := smtp.NewClient(conn, s.cfg.SMTPHost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Start TLS
|
||||
if err := client.StartTLS(tlsConfig); err != nil {
|
||||
return fmt.Errorf("failed to start TLS: %w", err)
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
auth := smtp.PlainAuth("", s.cfg.SMTPUser, s.cfg.SMTPPass, s.cfg.SMTPHost)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("failed to authenticate: %w", err)
|
||||
return fmt.Errorf("failed to authenticate with %s: %w", s.cfg.SMTPUser, err)
|
||||
}
|
||||
|
||||
// Set recipient and send
|
||||
// Set sender
|
||||
if err := client.Mail(s.cfg.SMTPUser); err != nil {
|
||||
return fmt.Errorf("failed to set mail from: %w", err)
|
||||
return fmt.Errorf("failed to set mail from %s: %w", s.cfg.SMTPUser, err)
|
||||
}
|
||||
|
||||
if err := client.Rcpt(email); err != nil {
|
||||
return fmt.Errorf("failed to set mail to: %w", err)
|
||||
// Set recipient
|
||||
if err := client.Rcpt(toEmail); err != nil {
|
||||
return fmt.Errorf("failed to set mail to %s: %w", toEmail, err)
|
||||
}
|
||||
|
||||
// Get data writer
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get data writer: %w", err)
|
||||
}
|
||||
defer wc.Close()
|
||||
|
||||
// Write message
|
||||
if _, err := wc.Write([]byte(message)); err != nil {
|
||||
return fmt.Errorf("failed to write message: %w", err)
|
||||
}
|
||||
|
||||
if err := client.Quit(); err != nil {
|
||||
return fmt.Errorf("failed to quit SMTP: %w", err)
|
||||
}
|
||||
// Quit - ignore quit errors since email was already queued
|
||||
_ = client.Quit()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConnection tests SMTP connection without sending email
|
||||
func (s *Sender) TestConnection() error {
|
||||
port, err := strconv.Atoi(s.cfg.SMTPPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SMTP port '%s': %w", s.cfg.SMTPPort, err)
|
||||
}
|
||||
|
||||
// Test TCP connection
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, port)
|
||||
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||
|
||||
// Test TLS connection
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: s.cfg.SMTPHost,
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TCP connection failed to %s: %w", addr, err)
|
||||
return fmt.Errorf("failed to dial TLS to %s: %w", addr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Test SMTP connection
|
||||
// Test SMTP client creation
|
||||
client, err := smtp.NewClient(conn, s.cfg.SMTPHost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||||
|
||||
@@ -156,6 +156,8 @@ func (h *Handler) Start(host, port string) error {
|
||||
mux.HandleFunc("/unsubscribe", h.unsubscribeHandler)
|
||||
mux.HandleFunc("/newsletters", h.newslettersHandler)
|
||||
mux.HandleFunc("/newsletter/", h.newsletterDetailHandler)
|
||||
mux.HandleFunc("/contact", h.contactHandler)
|
||||
mux.HandleFunc("/about", h.aboutHandler)
|
||||
|
||||
// Wrap with logging middleware
|
||||
handler := h.loggingMiddleware(mux)
|
||||
@@ -331,6 +333,178 @@ func (h *Handler) newsletterDetailHandler(
|
||||
tmpl.ExecuteTemplate(w, "base.html", newsletter)
|
||||
}
|
||||
|
||||
func (h *Handler) contactHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
tmpl, err := template.ParseFiles(
|
||||
h.getTemplatePath("base.html"),
|
||||
h.getTemplatePath("contact.html"),
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"IsContact": true,
|
||||
}
|
||||
|
||||
tmpl.ExecuteTemplate(w, "base.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
h.handleContactSubmission(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleContactSubmission(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "Failed to parse form data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract form fields
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
email := strings.TrimSpace(r.FormValue("email"))
|
||||
subject := strings.TrimSpace(r.FormValue("subject"))
|
||||
message := strings.TrimSpace(r.FormValue("message"))
|
||||
subscribe := r.FormValue("subscribe") == "on"
|
||||
|
||||
// Validate required fields
|
||||
if name == "" || email == "" || subject == "" || message == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "All fields are required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate email format (basic validation)
|
||||
if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "Invalid email address",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate message length (prevent spam)
|
||||
if len(message) < 10 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "Message must be at least 10 characters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(message) > 5000 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "Message must be less than 5000 characters",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If subscribe checkbox is checked, add to subscribers
|
||||
if subscribe {
|
||||
if err := h.db.AddSubscriber(r.Context(), email); err != nil {
|
||||
// Log but don't fail the contact submission
|
||||
h.logger.Printf(
|
||||
"ℹ Subscriber %s already exists or failed to add: %v",
|
||||
email,
|
||||
err,
|
||||
)
|
||||
} else {
|
||||
h.logger.Printf("✓ New subscriber added: %s", email)
|
||||
}
|
||||
}
|
||||
|
||||
// Send confirmation email to the user
|
||||
if err := h.email.SendContactConfirmation(email, name); err != nil {
|
||||
h.logger.Printf(
|
||||
"❌ Failed to send contact confirmation to %s: %v",
|
||||
email,
|
||||
err,
|
||||
)
|
||||
} else {
|
||||
h.logger.Printf("✓ Contact confirmation email sent to %s", email)
|
||||
}
|
||||
|
||||
// Send notification email to admin
|
||||
adminEmail := h.cfg.AdminEmail
|
||||
if adminEmail != "" {
|
||||
if err := h.email.SendContactNotification(
|
||||
adminEmail,
|
||||
name,
|
||||
email,
|
||||
subject,
|
||||
message,
|
||||
); err != nil {
|
||||
h.logger.Printf(
|
||||
"❌ Failed to send contact notification to admin: %v",
|
||||
err,
|
||||
)
|
||||
} else {
|
||||
h.logger.Printf("✓ Contact notification sent to admin: %s", adminEmail)
|
||||
}
|
||||
}
|
||||
|
||||
// Save contact message to database (optional - add to your DB interface)
|
||||
if err := h.db.AddContactMessage(r.Context(), name, email, subject, message); err != nil {
|
||||
h.logger.Printf(
|
||||
"⚠ Failed to save contact message: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
h.logger.Printf("✓ Contact form submitted by %s (%s)", name, email)
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Thank you for your message. We'll get back to you soon!",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) aboutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(
|
||||
h.getTemplatePath("base.html"),
|
||||
h.getTemplatePath("about.html"),
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to parse template: %v", err), http.StatusInternalServerError)
|
||||
h.logger.Printf("❌ Template parse error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"IsAbout": true,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
tmpl.ExecuteTemplate(w, "base.html", data)
|
||||
}
|
||||
|
||||
func getBaseURL(r *http.Request) string {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
|
||||
Reference in New Issue
Block a user