feat: added lots of work to landing page

This commit is contained in:
Blake Ridgway
2025-11-19 09:03:29 -06:00
parent 57e09ceea9
commit ac1d18f3a3
8 changed files with 1704 additions and 339 deletions

View File

@@ -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 == "" {

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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 {

File diff suppressed because it is too large Load Diff

330
templates/about.html Normal file
View File

@@ -0,0 +1,330 @@
{{define "about"}}
<div class="contact-about-page">
<div class="hero-section">
<h1>About RideAware</h1>
<p>Smart cycling training for every level</p>
</div>
<div class="about-content">
<div class="about-text">
<h2>Our Mission</h2>
<p>
RideAware is dedicated to making cycling training accessible,
effective, and enjoyable for cyclists of all levels. We provide
intelligent training plans, real-time analytics, and community support
to help you achieve your cycling goals.
</p>
<p>
Every ride counts. We believe smart training combined with technology
can unlock your full potential as a cyclist.
</p>
<ul>
<li>AI-powered adaptive training plans</li>
<li>Real-time performance analytics</li>
<li>Expert coaching and guidance</li>
<li>Community-driven motivation</li>
<li>Seamless device integration</li>
</ul>
</div>
<div class="about-image">
<div style="
width: 100%;
height: 400px;
background: var(--gradient);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 4rem;
">
<i class="fas fa-bicycle"></i>
</div>
</div>
</div>
<div class="values-section">
<div class="section-header">
<h2>Our Values</h2>
<p>What drives our mission</p>
</div>
<div class="values-grid">
<div class="value-card">
<div class="value-icon">
<i class="fas fa-heart"></i>
</div>
<h3>Passion</h3>
<p>
We're cyclists ourselves. We understand the dedication it takes
to improve and achieve your goals.
</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-brain"></i>
</div>
<h3>Intelligence</h3>
<p>
Our AI-driven platform learns from your performance to deliver
personalized training that actually works.
</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-users"></i>
</div>
<h3>Community</h3>
<p>
Cycling is better together. Connect with other riders, share
achievements, and push each other forward.
</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-chart-line"></i>
</div>
<h3>Transparency</h3>
<p>
See all your data clearly. We believe in giving you the insights
you need to understand your progress.
</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-lightbulb"></i>
</div>
<h3>Innovation</h3>
<p>
Technology should enhance your cycling, not complicate it.
We're constantly improving to serve you better.
</p>
</div>
<div class="value-card">
<div class="value-icon">
<i class="fas fa-medal"></i>
</div>
<h3>Excellence</h3>
<p>
Whether you're training for a race or personal satisfaction,
we help you reach peak performance.
</p>
</div>
</div>
</div>
<div class="team-section">
<div class="team-container">
<div class="team-header">
<h2>Meet Our Team</h2>
<p>Cyclists and engineers building the future of training</p>
</div>
<div class="team-grid">
<div class="team-member">
<div class="team-member-image">
<i class="fas fa-user-circle"></i>
</div>
<div class="team-member-info">
<h3>Blake Ridgway</h3>
<p>Founder & CEO</p>
<div class="bio">
Building the future of cycling training with scalable infrastructure
and performant systems. Passionate about Infrastructure-as-Code,
cloud networking, and creating observable platforms that ship faster
with confidence.
</div>
</div>
</div>
<div class="team-member">
<div class="team-member-image">
<i class="fas fa-user-circle"></i>
</div>
<div class="team-member-info">
<h3>Cycling Experts</h3>
<p>Training Advisors</p>
<div class="bio">
Professional cyclists and coaches ensuring our training plans
are effective and science-based.
</div>
</div>
</div>
<div class="team-member">
<div class="team-member-image">
<i class="fas fa-user-circle"></i>
</div>
<div class="team-member-info">
<h3>You</h3>
<p>Community</p>
<div class="bio">
Every rider using RideAware is part of our team. Your feedback
shapes our future.
</div>
</div>
</div>
</div>
</div>
</div>
<div class="stats-section">
<div class="section-header">
<h2>By The Numbers</h2>
<p>Growth and impact</p>
</div>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-number">Coming</div>
<div class="stat-label">Q4 2026</div>
</div>
<div class="stat-box">
<div class="stat-number"></div>
<div class="stat-label">Potential</div>
</div>
<div class="stat-box">
<div class="stat-number">100%</div>
<div class="stat-label">Passion</div>
</div>
<div class="stat-box">
<div class="stat-number">You</div>
<div class="stat-label">In Control</div>
</div>
</div>
</div>
<div class="faq-section">
<div class="section-header">
<h2>Frequently Asked Questions</h2>
</div>
<div class="faq-container">
<div class="faq-item">
<div class="faq-question">
<h3>When is RideAware launching?</h3>
<i class="fas fa-chevron-down"></i>
</div>
<div class="faq-answer">
<p>
We're launching Q4 2026! Sign up for our newsletter to get
early access and exclusive launch day bonuses.
</p>
</div>
</div>
<div class="faq-item">
<div class="faq-question">
<h3>How much will it cost?</h3>
<i class="fas fa-chevron-down"></i>
</div>
<div class="faq-answer">
<p>
Pricing details coming soon. We're committed to making RideAware
accessible to cyclists at all price points.
</p>
</div>
</div>
<div class="faq-item">
<div class="faq-question">
<h3>What devices does RideAware support?</h3>
<i class="fas fa-chevron-down"></i>
</div>
<div class="faq-answer">
<p>
RideAware works on iOS, Android, web, and integrates with all
major fitness trackers and cycling computers (Garmin, Wahoo, etc.).
</p>
</div>
</div>
<div class="faq-item">
<div class="faq-question">
<h3>Is my data private?</h3>
<i class="fas fa-chevron-down"></i>
</div>
<div class="faq-answer">
<p>
Yes. Your training data is yours alone. We'll never sell or share
your personal information with third parties.
</p>
</div>
</div>
<div class="faq-item">
<div class="faq-question">
<h3>Can I import my current training data?</h3>
<i class="fas fa-chevron-down"></i>
</div>
<div class="faq-answer">
<p>
Yes! RideAware will integrate with Strava, TrainingPeaks, and other
platforms so you can bring all your history with you.
</p>
</div>
</div>
</div>
</div>
<div class="cta-section" style="
background: var(--gradient);
padding: 4rem 2rem;
text-align: center;
color: white;
border-radius: 16px;
margin: 4rem 2rem;
">
<h2>Ready to Elevate Your Cycling?</h2>
<p style="opacity: 0.9; margin: 1rem 0 2rem;">
Join the waitlist and be first to know when we launch
</p>
<a href="/" class="action-btn primary" style="
background: white;
color: var(--primary);
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 50px;
padding: 0.8rem 2rem;
text-decoration: none;
font-weight: 600;
">
Join the Waitlist
<i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
<script>
// FAQ accordion toggle
document.querySelectorAll('.faq-question').forEach((question) => {
question.addEventListener('click', () => {
const item = question.parentElement;
const answer = item.querySelector('.faq-answer');
const isOpen = item.classList.contains('open');
// Close all other items
document.querySelectorAll('.faq-item').forEach((faq) => {
faq.classList.remove('open');
});
// Toggle current item
if (!isOpen) {
item.classList.add('open');
}
});
});
</script>
{{end}}

View File

@@ -1,136 +1,145 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title>{{block "title" .}}RideAware{{end}}</title>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title>{{block "title" .}}RideAware{{end}}</title>
<!-- Icons/Fonts -->
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<!-- Icons/Fonts -->
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet"
/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<!-- Core CSS -->
<link
rel="stylesheet"
href="/static/css/styles.css"
/>
<!-- Core CSS -->
<link rel="stylesheet" href="/static/css/styles.css" />
<!-- Favicons -->
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/static/assets/32x32.png"
/>
<link
rel="alternate icon"
type="image/png"
sizes="32x32"
href="/static/assets/32x32.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/static/assets/apple-touch-icon.png"
/>
<link
rel="manifest"
href="/static/assets/site.webmanifest"
/>
<meta name="theme-color" content="#0f172a" />
<!-- Favicons -->
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/static/assets/32x32.png"
/>
<link
rel="alternate icon"
type="image/png"
sizes="32x32"
href="/static/assets/32x32.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/static/assets/apple-touch-icon.png"
/>
<link rel="manifest" href="/static/assets/site.webmanifest" />
<meta name="theme-color" content="#0f172a" />
{{block "extra_head" .}}{{end}}
</head>
<body>
<nav class="navbar">
<div class="nav-container">
<a
href="/"
class="logo"
aria-label="RideAware home"
>
<img
src="/static/assets/logo.png"
alt="RideAware"
class="logo-img"
width="140"
height="28"
decoding="async"
fetchpriority="high"
/>
</a>
{{block "extra_head" .}}{{end}}
</head>
<body>
<nav class="navbar">
<div class="nav-container">
<a href="/" class="logo" aria-label="RideAware home">
<img
src="/static/assets/logo.png"
alt="RideAware"
class="logo-img"
width="140"
height="28"
decoding="async"
fetchpriority="high"
/>
</a>
<button
class="nav-toggle"
id="nav-toggle"
aria-label="Toggle navigation"
aria-controls="primary-nav"
aria-expanded="false"
>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
</div>
</nav>
<ul class="nav-links" id="primary-nav">
<li><a href="/#features">Features</a></li>
<li><a href="/newsletters">Newsletters</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
{{block "content" .}}{{end}}
<button
class="nav-toggle"
id="nav-toggle"
aria-label="Toggle navigation"
aria-controls="primary-nav"
aria-expanded="false"
>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
</div>
</nav>
<footer class="footer">
<p>&copy; 2025 RideAware. All rights reserved.</p>
</footer>
{{block "content" .}}
{{if .IsContact}}
{{template "contact" .}}
{{else if .IsAbout}}
{{template "about" .}}
{{else if .IsHome}}
{{template "index" .}}
{{else}}
{{template "newsletters" .}}
{{end}}
{{end}}
<!-- Core JS -->
<script
defer
src="https://cdn.statically.io/gl/rideaware/landing/06d19988c7df53636277f945f9ed853bda76471b/static/js/main.min.js"
crossorigin="anonymous"
></script>
<footer class="footer">
<p>&copy; 2025 RideAware. All rights reserved.</p>
</footer>
{{block "extra_scripts" .}}
<script>
(function () {
const btn = document.getElementById("nav-toggle");
const menu = document.getElementById("primary-nav");
if (!btn || !menu) return;
<!-- Core JS -->
<script
defer
src="https://cdn.statically.io/gl/rideaware/landing/06d19988c7df53636277f945f9ed853bda76471b/static/js/main.min.js"
crossorigin="anonymous"
></script>
function closeMenu() {
btn.classList.remove("active");
btn.setAttribute("aria-expanded", "false");
menu.classList.remove("open");
{{block "extra_scripts" .}}
<script>
(function () {
const btn = document.getElementById("nav-toggle");
const menu = document.getElementById("primary-nav");
if (!btn || !menu) return;
function closeMenu() {
btn.classList.remove("active");
btn.setAttribute("aria-expanded", "false");
menu.classList.remove("open");
}
btn.addEventListener("click", () => {
const open = btn.classList.toggle("active");
btn.setAttribute("aria-expanded", String(open));
menu.classList.toggle("open", open);
});
menu.addEventListener("click", (e) => {
if (e.target.tagName === "A") closeMenu();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeMenu();
});
document.addEventListener("click", (e) => {
if (!menu.contains(e.target) && !btn.contains(e.target)) {
closeMenu();
}
btn.addEventListener("click", () => {
const open = btn.classList.toggle("active");
btn.setAttribute("aria-expanded", String(open));
menu.classList.toggle("open", open);
});
menu.addEventListener("click", (e) => {
if (e.target.tagName === "A") closeMenu();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeMenu();
});
document.addEventListener("click", (e) => {
if (!menu.contains(e.target) && !btn.contains(e.target)) {
closeMenu();
}
});
})();
</script>
{{end}}
</body>
});
})();
</script>
{{end}}
</body>
</html>

171
templates/contact.html Normal file
View File

@@ -0,0 +1,171 @@
{{define "contact"}}
<div class="contact-about-page">
<div class="hero-section">
<h1>Get in Touch</h1>
<p>We'd love to hear from you. Send us a message!</p>
</div>
<div class="about-content">
<div class="about-text">
<h2>Let's Connect</h2>
<p>
Have a question about RideAware? Want to collaborate?
Reach out and let us know how we can help.
</p>
<ul>
<li>Fast response times</li>
<li>Friendly support team</li>
<li>Multiple contact options</li>
<li>Always here to help</li>
</ul>
</div>
<form class="contact-form" id="contactForm">
<div class="form-success" id="successMessage">
<strong>Thank you!</strong> We've received your message
and will get back to you soon.
</div>
<h2>Send us a message</h2>
<div class="form-group">
<label for="name">
Full Name <span class="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
autocomplete="name"
/>
</div>
<div class="form-group">
<label for="email">
Email Address <span class="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
/>
<small>We'll respond to this email address</small>
</div>
<div class="form-group">
<label for="subject">Subject <span class="required">*</span></label>
<select id="subject" name="subject" required>
<option value="">-- Select a subject --</option>
<option value="general">General Inquiry</option>
<option value="support">Support</option>
<option value="partnership">Partnership</option>
<option value="feedback">Feedback</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="message">
Message <span class="required">*</span>
</label>
<textarea
id="message"
name="message"
required
placeholder="Your message here..."
></textarea>
</div>
<div class="newsletter-opt-in">
<label for="subscribe" class="checkbox-label">
<input
type="checkbox"
id="subscribe"
name="subscribe"
class="checkbox-input"
/>
<span class="checkbox-text">
<i class="fas fa-bell"></i>
Subscribe to our newsletter for training tips and updates
</span>
</label>
</div>
<button type="submit" class="form-submit">
<i class="fas fa-paper-plane"></i>
Send Message
</button>
</form>
</div>
<div class="contact-info">
<div class="info-card">
<div class="info-card-icon">
<i class="fas fa-envelope"></i>
</div>
<h3>Email</h3>
<p>
<a href="mailto:hello@rideaware.com"
>hello@rideaware.com</a
>
</p>
</div>
<div class="info-card">
<div class="info-card-icon">
<i class="fas fa-map-marker-alt"></i>
</div>
<h3>Address</h3>
<p>1909 W Owen K Garriott Rd<br />Enid, OK 73703</p>
</div>
</div>
</div>
<script>
document.getElementById('contactForm').addEventListener(
'submit',
async (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
console.log('Form submitted');
console.log('Form data:', Object.fromEntries(formData));
try {
const response = await fetch('/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(formData),
});
console.log('Response status:', response.status);
const data = await response.json();
console.log('Response data:', data);
if (response.ok) {
document.getElementById('successMessage')
.classList.add('show');
form.reset();
setTimeout(() => {
document.getElementById('successMessage')
.classList.remove('show');
}, 5000);
} else {
alert('Error: ' + (data.error || 'Failed to send message'));
}
} catch (error) {
console.error('Error:', error);
alert('Failed to send message: ' + error.message);
}
}
);
</script>
{{end}}