rewrite number whatever to .net and blazor.
This commit is contained in:
310
Pages/About.cshtml
Normal file
310
Pages/About.cshtml
Normal file
@@ -0,0 +1,310 @@
|
||||
@page
|
||||
@model AboutModel
|
||||
@{
|
||||
ViewData["Title"] = "RideAware - About";
|
||||
}
|
||||
|
||||
<section class="page-hero">
|
||||
<h1>About RideAware</h1>
|
||||
<p>Smart cycling training for every level</p>
|
||||
</section>
|
||||
|
||||
<section class="about-mission">
|
||||
<div class="container">
|
||||
<div class="mission-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="mission-visual">
|
||||
<i class="fas fa-bicycle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="values-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<div class="section-label"><i class="fas fa-compass"></i> Values</div>
|
||||
<h2 class="section-title">What drives our mission</h2>
|
||||
</div>
|
||||
|
||||
<div class="values-grid">
|
||||
<div class="value-card fade-in">
|
||||
<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 fade-in">
|
||||
<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 fade-in">
|
||||
<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 fade-in">
|
||||
<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 fade-in">
|
||||
<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 fade-in">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="team-section">
|
||||
<div class="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 fade-in">
|
||||
<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 fade-in">
|
||||
<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 fade-in">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="stats-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">By The Numbers</h2>
|
||||
<p class="section-subtitle" style="margin: 0 auto;">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>
|
||||
</section>
|
||||
|
||||
<section class="faq-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">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>
|
||||
</section>
|
||||
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-card">
|
||||
<h2>Ready to Elevate Your Cycling?</h2>
|
||||
<p>Join the waitlist and be first to know when we launch.</p>
|
||||
<a href="/" class="action-btn primary">
|
||||
Join the Waitlist
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// FAQ accordion toggle
|
||||
document.querySelectorAll('.faq-question').forEach((question) => {
|
||||
question.addEventListener('click', () => {
|
||||
const item = question.parentElement;
|
||||
const isOpen = item.classList.contains('open');
|
||||
|
||||
document.querySelectorAll('.faq-item').forEach((faq) => {
|
||||
faq.classList.remove('open');
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
item.classList.add('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
10
Pages/About.cshtml.cs
Normal file
10
Pages/About.cshtml.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
public class AboutModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
224
Pages/Contact.cshtml
Normal file
224
Pages/Contact.cshtml
Normal file
@@ -0,0 +1,224 @@
|
||||
@page
|
||||
@model ContactModel
|
||||
@{
|
||||
ViewData["Title"] = "RideAware - Contact";
|
||||
}
|
||||
|
||||
<section class="page-hero">
|
||||
<h1>Get in Touch</h1>
|
||||
<p>We'd love to hear from you. Send us a message!</p>
|
||||
</section>
|
||||
|
||||
<section class="contact-layout">
|
||||
<div class="container">
|
||||
<div class="contact-info-side">
|
||||
<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 class="contact-info">
|
||||
<div class="info-card">
|
||||
<div class="info-card-icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Email</h3>
|
||||
<p><a href="mailto:hello@rideaware.com">hello@rideaware.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<div class="info-card-icon">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Address</h3>
|
||||
<p>1909 W Owen K Garriott Rd<br />Enid, OK 73703</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Simple Addition CAPTCHA -->
|
||||
<div class="form-group">
|
||||
<label for="captcha">
|
||||
Quick Math Check <span class="required">*</span>
|
||||
</label>
|
||||
<div id="captchaQuestion" style="font-weight: bold; margin-bottom: 8px; color: var(--text-primary); font-size: 16px;"></div>
|
||||
<input
|
||||
type="text"
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
required
|
||||
placeholder="Enter the answer"
|
||||
autocomplete="off"
|
||||
inputmode="numeric"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="form-submit">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
let currentCaptcha = null;
|
||||
|
||||
function generateCaptcha() {
|
||||
const num1 = Math.floor(Math.random() * 20) + 1;
|
||||
const num2 = Math.floor(Math.random() * 20) + 1;
|
||||
const answer = num1 + num2;
|
||||
|
||||
return {
|
||||
question: `${num1} + ${num2} = ?`,
|
||||
answer: answer.toString()
|
||||
};
|
||||
}
|
||||
|
||||
function initCaptcha() {
|
||||
currentCaptcha = generateCaptcha();
|
||||
document.getElementById('captchaQuestion').textContent = currentCaptcha.question;
|
||||
document.getElementById('captcha').value = '';
|
||||
document.getElementById('captcha').focus();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCaptcha);
|
||||
} else {
|
||||
initCaptcha();
|
||||
}
|
||||
|
||||
document.getElementById('contactForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const userAnswer = document.getElementById('captcha').value.trim();
|
||||
if (userAnswer !== currentCaptcha.answer) {
|
||||
alert('Incorrect answer. Please try again.');
|
||||
currentCaptcha = generateCaptcha();
|
||||
document.getElementById('captchaQuestion').textContent = currentCaptcha.question;
|
||||
document.getElementById('captcha').value = '';
|
||||
document.getElementById('captcha').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
formData.delete('captcha');
|
||||
|
||||
try {
|
||||
const response = await fetch('/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(formData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('successMessage').classList.add('show');
|
||||
form.reset();
|
||||
initCaptcha();
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('successMessage').classList.remove('show');
|
||||
}, 5000);
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to send message'));
|
||||
initCaptcha();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to send message: ' + error.message);
|
||||
initCaptcha();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
158
Pages/Contact.cshtml.cs
Normal file
158
Pages/Contact.cshtml.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using landing.Data;
|
||||
using landing.Models;
|
||||
using landing.Services;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class ContactModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly EmailService _email;
|
||||
private readonly SpamDetectionService _spam;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly ILogger<ContactModel> _logger;
|
||||
|
||||
public ContactModel(
|
||||
AppDbContext db,
|
||||
EmailService email,
|
||||
SpamDetectionService spam,
|
||||
IConfiguration config,
|
||||
ILogger<ContactModel> logger)
|
||||
{
|
||||
_db = db;
|
||||
_email = email;
|
||||
_spam = spam;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
var name = Request.Form["name"].ToString().Trim();
|
||||
var email = Request.Form["email"].ToString().Trim();
|
||||
var subject = Request.Form["subject"].ToString().Trim();
|
||||
var message = Request.Form["message"].ToString().Trim();
|
||||
var subscribe = Request.Form["subscribe"].ToString() == "on";
|
||||
|
||||
// Validate required fields
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(email) ||
|
||||
string.IsNullOrEmpty(subject) || string.IsNullOrEmpty(message))
|
||||
{
|
||||
return JsonError("All fields are required", 400);
|
||||
}
|
||||
|
||||
if (!_spam.IsValidName(name))
|
||||
{
|
||||
_logger.LogWarning("Rejected submission: Invalid name format - {Name}", name);
|
||||
return JsonError("Please provide a valid name", 400);
|
||||
}
|
||||
|
||||
if (!_spam.IsValidEmail(email))
|
||||
{
|
||||
_logger.LogWarning("Rejected submission: Invalid email format - {Email}", email);
|
||||
return JsonError("Please provide a valid email address", 400);
|
||||
}
|
||||
|
||||
if (!_spam.IsValidSubject(subject))
|
||||
{
|
||||
_logger.LogWarning("Rejected submission: Invalid subject - {Subject}", subject);
|
||||
return JsonError("Please select a valid subject", 400);
|
||||
}
|
||||
|
||||
if (message.Length < 10)
|
||||
return JsonError("Message must be at least 10 characters", 400);
|
||||
|
||||
if (message.Length > 5000)
|
||||
return JsonError("Message must be less than 5000 characters", 400);
|
||||
|
||||
if (!_spam.IsEnglishText(message))
|
||||
{
|
||||
_logger.LogWarning("Rejected submission: Non-English message from {Name} ({Email})", name, email);
|
||||
return JsonError("Please submit your message in English", 400);
|
||||
}
|
||||
|
||||
if (_spam.IsSpamMessage(message))
|
||||
{
|
||||
_logger.LogWarning("Rejected spam submission from {Name} ({Email})", name, email);
|
||||
return JsonError("Your message was flagged as spam. Please try again with a different message.", 400);
|
||||
}
|
||||
|
||||
// If subscribe checkbox is checked, add to subscribers
|
||||
if (subscribe)
|
||||
{
|
||||
try
|
||||
{
|
||||
_db.Subscribers.Add(new Subscriber { Email = email });
|
||||
await _db.SaveChangesAsync();
|
||||
_logger.LogInformation("New subscriber added: {Email}", email);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogInformation("Subscriber {Email} already exists or failed to add", email);
|
||||
}
|
||||
}
|
||||
|
||||
// Send confirmation email to the user
|
||||
try
|
||||
{
|
||||
await _email.SendContactConfirmationAsync(email, name);
|
||||
_logger.LogInformation("Contact confirmation email sent to {Email}", email);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to send contact confirmation to {Email}: {Error}", email, ex.Message);
|
||||
}
|
||||
|
||||
// Send notification email to admin
|
||||
var adminEmail = _config["ADMIN_EMAIL"];
|
||||
if (!string.IsNullOrEmpty(adminEmail))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _email.SendContactNotificationAsync(adminEmail, name, email, subject, message);
|
||||
_logger.LogInformation("Contact notification sent to admin: {AdminEmail}", adminEmail);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to send contact notification to admin: {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Save contact message to database
|
||||
try
|
||||
{
|
||||
_db.ContactMessages.Add(new ContactMessage
|
||||
{
|
||||
Name = name,
|
||||
Email = email,
|
||||
Subject = subject,
|
||||
Message = message,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to save contact message: {Error}", ex.Message);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Contact form submitted by {Name} ({Email})", name, email);
|
||||
|
||||
Response.StatusCode = 201;
|
||||
return new JsonResult(new { message = "Thank you for your message. We'll get back to you soon!" });
|
||||
}
|
||||
|
||||
private IActionResult JsonError(string error, int statusCode)
|
||||
{
|
||||
Response.StatusCode = statusCode;
|
||||
return new JsonResult(new { error });
|
||||
}
|
||||
}
|
||||
26
Pages/Error.cshtml
Normal file
26
Pages/Error.cshtml
Normal file
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
27
Pages/Error.cshtml.cs
Normal file
27
Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
262
Pages/Index.cshtml
Normal file
262
Pages/Index.cshtml
Normal file
@@ -0,0 +1,262 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "RideAware - Smart Cycling Training Platform";
|
||||
}
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">
|
||||
<span class="pulse-dot"></span>
|
||||
Development Build Available
|
||||
</div>
|
||||
|
||||
<h1>Elevate Your<br /><span class="gradient-text">Cycling Journey</span></h1>
|
||||
|
||||
<p class="subtitle">
|
||||
The smart training platform for cyclists who demand excellence.
|
||||
AI-powered plans, real-time analytics, and community-driven motivation.
|
||||
</p>
|
||||
|
||||
<div class="hero-cta">
|
||||
<input
|
||||
type="email"
|
||||
class="email-input"
|
||||
id="email-input"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
<button class="btn-primary" id="notify-button">Subscribe</button>
|
||||
</div>
|
||||
|
||||
<p class="hero-note">Join the newsletter for feature updates and early access.</p>
|
||||
|
||||
<a href="https://dev.rideaware.org" class="action-btn secondary" target="_blank" style="margin-top: 1rem;">
|
||||
<i class="fas fa-rocket"></i>
|
||||
Access Development Build
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Social Proof -->
|
||||
<section class="proof-bar">
|
||||
<div class="container">
|
||||
<div class="proof-item">
|
||||
<div class="number">Q4 2026</div>
|
||||
<div class="label">Launch Target</div>
|
||||
</div>
|
||||
<div class="proof-item">
|
||||
<div class="number">6+</div>
|
||||
<div class="label">Core Features</div>
|
||||
</div>
|
||||
<div class="proof-item">
|
||||
<div class="number">AI</div>
|
||||
<div class="label">Powered Training</div>
|
||||
</div>
|
||||
<div class="proof-item">
|
||||
<div class="number">100%</div>
|
||||
<div class="label">Data Privacy</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features -->
|
||||
<section class="features" id="features">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<div class="section-label"><i class="fas fa-sparkles"></i> Features</div>
|
||||
<h2 class="section-title">Everything you need to train smarter</h2>
|
||||
<p class="section-subtitle">
|
||||
Comprehensive tools to optimize your cycling performance, from planning to recovery.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card fade-in">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<h3>Smart Training Plans</h3>
|
||||
<p>AI-powered plans that adapt to your goals, fitness level, and schedule automatically.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<h3>Advanced Analytics</h3>
|
||||
<p>Interactive charts, progress insights, and detailed performance metrics over time.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-bicycle"></i>
|
||||
</div>
|
||||
<h3>Virtual Training</h3>
|
||||
<p>Expert coaching and immersive structured workouts designed for peak performance.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-heart"></i>
|
||||
</div>
|
||||
<h3>Health & Recovery</h3>
|
||||
<p>Nutrition tracking, recovery optimization, and proactive injury prevention tools.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<h3>Community & Social</h3>
|
||||
<p>Connect with fellow cyclists, share achievements, and climb the leaderboards.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card fade-in">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</div>
|
||||
<h3>Smart Integration</h3>
|
||||
<p>Sync with Garmin, Wahoo, and other wearables. Import from Strava and TrainingPeaks.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works -->
|
||||
<section class="how-it-works">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<div class="section-label"><i class="fas fa-route"></i> How It Works</div>
|
||||
<h2 class="section-title">Three steps to better training</h2>
|
||||
<p class="section-subtitle">
|
||||
Get started in minutes and let RideAware handle the complexity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="steps-grid">
|
||||
<div class="step-card fade-in">
|
||||
<div class="step-number-ring">
|
||||
<span class="gradient-text" style="font-weight: 700; font-size: 1.1rem;">1</span>
|
||||
</div>
|
||||
<h3>Sign Up</h3>
|
||||
<p>Create your profile and set your cycling goals and fitness level.</p>
|
||||
</div>
|
||||
|
||||
<div class="step-card fade-in">
|
||||
<div class="step-number-ring">
|
||||
<span class="gradient-text" style="font-weight: 700; font-size: 1.1rem;">2</span>
|
||||
</div>
|
||||
<h3>Set Goals</h3>
|
||||
<p>Define what you want to achieve and let our AI build your plan.</p>
|
||||
</div>
|
||||
|
||||
<div class="step-card fade-in">
|
||||
<div class="step-number-ring">
|
||||
<span class="gradient-text" style="font-weight: 700; font-size: 1.1rem;">3</span>
|
||||
</div>
|
||||
<h3>Train Smarter</h3>
|
||||
<p>Follow your adaptive plan, track progress, and crush your goals.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Roadmap -->
|
||||
<section class="roadmap">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<div class="section-label"><i class="fas fa-code-branch"></i> Roadmap</div>
|
||||
<h2 class="section-title">Development Status</h2>
|
||||
<p class="section-subtitle">
|
||||
RideAware is actively being built. Track our progress and help shape the platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="timeline-item live">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">Live</div>
|
||||
<h4>User Authentication</h4>
|
||||
<p>Sign up, login, and user profiles fully functional</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item live">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">Live</div>
|
||||
<h4>Equipment Management</h4>
|
||||
<p>Add and manage your bikes and cycling gear</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item live">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">Live</div>
|
||||
<h4>Training Zones</h4>
|
||||
<p>HR and power zone calculations</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item in-progress">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">In Progress</div>
|
||||
<h4>Workout Planning</h4>
|
||||
<p>Structured workout builder with interval support</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item in-progress">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">In Progress</div>
|
||||
<h4>Performance Analytics</h4>
|
||||
<p>Detailed performance tracking and metrics</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item planned">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">Planned</div>
|
||||
<h4>Device Integration</h4>
|
||||
<p>Sync with wearables and cycling computers</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item planned">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">Planned</div>
|
||||
<h4>Community Features</h4>
|
||||
<p>Social sharing and competitive leaderboards</p>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item planned">
|
||||
<div class="timeline-dot"></div>
|
||||
<div class="timeline-badge">Planned</div>
|
||||
<h4>Advanced Training</h4>
|
||||
<p>AI-powered coaching and personalized plans</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 3rem;">
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1rem;">Try what's live today</p>
|
||||
<a href="https://dev.rideaware.org" class="action-btn primary" target="_blank">
|
||||
<i class="fas fa-rocket"></i>
|
||||
Access Development Build
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-card">
|
||||
<h2>Ready to elevate your cycling?</h2>
|
||||
<p>Join the waitlist and be first to know when we launch.</p>
|
||||
<div class="hero-cta">
|
||||
<input
|
||||
type="email"
|
||||
class="email-input"
|
||||
id="cta-email-input"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
<button class="btn-primary" id="cta-notify-button">Subscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
10
Pages/Index.cshtml.cs
Normal file
10
Pages/Index.cshtml.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
101
Pages/NewsletterDetail.cshtml
Normal file
101
Pages/NewsletterDetail.cshtml
Normal file
@@ -0,0 +1,101 @@
|
||||
@page "/newsletter/{id:int}"
|
||||
@model NewsletterDetailModel
|
||||
@{
|
||||
ViewData["Title"] = $"RideAware - {Model.NewsletterItem?.Subject}";
|
||||
}
|
||||
|
||||
@if (Model.NewsletterItem != null)
|
||||
{
|
||||
<div class="article-wrap">
|
||||
<aside class="article-aside">
|
||||
<a href="/newsletters" class="back-link">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Newsletters
|
||||
</a>
|
||||
|
||||
<div class="article-meta">
|
||||
<h2 class="article-title">
|
||||
@Model.NewsletterItem.Subject
|
||||
</h2>
|
||||
|
||||
<div class="meta-row">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>@Model.NewsletterItem.SentAt.ToString("MMMM d, yyyy 'at' h:mm tt")</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="toc">
|
||||
<div class="toc-title">On this page</div>
|
||||
<ol id="toc-list"></ol>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="article-main">
|
||||
<header class="article-hero">
|
||||
<div class="newsletter-icon">
|
||||
<i class="fas fa-envelope-open-text"></i>
|
||||
</div>
|
||||
<h1>@Model.NewsletterItem.Subject</h1>
|
||||
</header>
|
||||
|
||||
<article class="newsletter-content" id="article">
|
||||
@Html.Raw(Model.NewsletterItem.Body)
|
||||
</article>
|
||||
|
||||
<div class="newsletter-actions">
|
||||
<a href="/newsletters" class="action-btn primary">
|
||||
<i class="fas fa-list"></i>
|
||||
View All Newsletters
|
||||
</a>
|
||||
<button onclick="window.print()" class="action-btn secondary">
|
||||
<i class="fas fa-print"></i>
|
||||
Print
|
||||
</button>
|
||||
<button onclick="shareNewsletter()" class="action-btn secondary">
|
||||
<i class="fas fa-share-alt"></i>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function shareNewsletter() {
|
||||
if (navigator.share) {
|
||||
navigator
|
||||
.share({ title: document.title, url: location.href })
|
||||
.catch(() => {});
|
||||
} else {
|
||||
navigator.clipboard.writeText(location.href);
|
||||
alert('Link copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
// Build TOC from h2/h3 inside the article
|
||||
(function buildTOC() {
|
||||
const article = document.getElementById('article');
|
||||
if (!article) return;
|
||||
|
||||
const headings = article.querySelectorAll('h2, h3');
|
||||
const list = document.getElementById('toc-list');
|
||||
if (!headings.length || !list) return;
|
||||
|
||||
headings.forEach((h, idx) => {
|
||||
const id = h.id || `h-${idx}`;
|
||||
h.id = id;
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = h.tagName === 'H2' ? 'toc-h2' : 'toc-h3';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = `#${id}`;
|
||||
a.textContent = h.textContent;
|
||||
|
||||
li.appendChild(a);
|
||||
list.appendChild(li);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
28
Pages/NewsletterDetail.cshtml.cs
Normal file
28
Pages/NewsletterDetail.cshtml.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using landing.Data;
|
||||
using landing.Models;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
public class NewsletterDetailModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public NewsletterDetailModel(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public Newsletter? NewsletterItem { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
NewsletterItem = await _db.Newsletters.FindAsync(id);
|
||||
|
||||
if (NewsletterItem == null)
|
||||
return NotFound("Newsletter not found");
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
78
Pages/Newsletters.cshtml
Normal file
78
Pages/Newsletters.cshtml
Normal file
@@ -0,0 +1,78 @@
|
||||
@page
|
||||
@model NewslettersModel
|
||||
@{
|
||||
ViewData["Title"] = "RideAware - Newsletters";
|
||||
}
|
||||
|
||||
<section class="page-header">
|
||||
<div class="page-header-content">
|
||||
<div class="header-icon">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
</div>
|
||||
<h1>RideAware Newsletters</h1>
|
||||
<p>
|
||||
Stay updated with the latest cycling tips, training insights, and
|
||||
product updates from our team.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="main-content">
|
||||
@if (Model.NewslettersList.Any())
|
||||
{
|
||||
<div class="newsletters-grid">
|
||||
@foreach (var newsletter in Model.NewslettersList)
|
||||
{
|
||||
<article class="newsletter-card">
|
||||
<div class="newsletter-header">
|
||||
<div class="newsletter-icon">
|
||||
<i class="fas fa-envelope-open-text"></i>
|
||||
</div>
|
||||
<div class="newsletter-info">
|
||||
<h2>
|
||||
<a href="/newsletter/@newsletter.Id">
|
||||
@newsletter.Subject
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="newsletter-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Sent on: @newsletter.SentAt.ToString("yyyy-MM-dd HH:mm:ss")</span>
|
||||
</div>
|
||||
|
||||
<div class="newsletter-excerpt">
|
||||
Get the latest updates on cycling training, performance tips,
|
||||
and RideAware features in this newsletter edition.
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/newsletter/@newsletter.Id"
|
||||
class="read-more-btn"
|
||||
>
|
||||
Read Full Newsletter
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-inbox"></i>
|
||||
</div>
|
||||
<h3>No Newsletters Yet</h3>
|
||||
<p>
|
||||
We're working on some amazing content for you. Subscribe to be the
|
||||
first to know when we publish our newsletters!
|
||||
</p>
|
||||
<a href="/" class="subscribe-prompt">
|
||||
<i class="fas fa-bell"></i>
|
||||
Subscribe for Updates
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
25
Pages/Newsletters.cshtml.cs
Normal file
25
Pages/Newsletters.cshtml.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using landing.Data;
|
||||
using landing.Models;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
public class NewslettersModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public NewslettersModel(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public List<Newsletter> NewslettersList { get; set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
NewslettersList = await _db.Newsletters
|
||||
.OrderByDescending(n => n.SentAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
125
Pages/Shared/_Layout.cshtml
Normal file
125
Pages/Shared/_Layout.cshtml
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>@(ViewData["Title"] ?? "RideAware")</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"
|
||||
/>
|
||||
|
||||
<!-- Core CSS -->
|
||||
<link rel="stylesheet" href="~/css/styles.css" />
|
||||
|
||||
<!-- Favicons -->
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="~/assets/32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="~/assets/apple-touch-icon.png"
|
||||
/>
|
||||
<link rel="manifest" href="~/assets/site.webmanifest" />
|
||||
<meta name="theme-color" content="#09090b" />
|
||||
|
||||
@await RenderSectionAsync("Head", required: false)
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="logo" aria-label="RideAware home">
|
||||
<img
|
||||
src="~/assets/logo.png"
|
||||
alt="RideAware"
|
||||
class="logo-img"
|
||||
width="120"
|
||||
height="24"
|
||||
decoding="async"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<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>
|
||||
|
||||
<button
|
||||
class="nav-toggle"
|
||||
id="nav-toggle"
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-controls="primary-nav"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2026 RideAware. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
<!-- Core JS -->
|
||||
<script defer src="~/js/main.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
@if (!IsSectionDefined("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 isOpen = btn.classList.toggle('active');
|
||||
btn.setAttribute('aria-expanded', String(isOpen));
|
||||
menu.classList.toggle('open', isOpen);
|
||||
});
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
2
Pages/Subscribe.cshtml
Normal file
2
Pages/Subscribe.cshtml
Normal file
@@ -0,0 +1,2 @@
|
||||
@page
|
||||
@model SubscribeModel
|
||||
76
Pages/Subscribe.cshtml.cs
Normal file
76
Pages/Subscribe.cshtml.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using landing.Data;
|
||||
using landing.Models;
|
||||
using landing.Services;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class SubscribeModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly EmailService _email;
|
||||
private readonly ILogger<SubscribeModel> _logger;
|
||||
|
||||
public SubscribeModel(AppDbContext db, EmailService email, ILogger<SubscribeModel> logger)
|
||||
{
|
||||
_db = db;
|
||||
_email = email;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IActionResult OnGet() => NotFound();
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
string? emailAddress = null;
|
||||
|
||||
// Try to read JSON body
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||
emailAddress = json.GetProperty("email").GetString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest(new { error = "Invalid request" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(emailAddress))
|
||||
{
|
||||
return BadRequest(new { error = "Email is required" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_db.Subscribers.Add(new Subscriber { Email = emailAddress });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest(new { error = "Email already exists" });
|
||||
}
|
||||
|
||||
// Build unsubscribe link
|
||||
var scheme = Request.Scheme;
|
||||
var host = Request.Host.Value;
|
||||
var unsubscribeLink = $"{scheme}://{host}/unsubscribe?email={emailAddress}";
|
||||
|
||||
try
|
||||
{
|
||||
await _email.SendConfirmationEmailAsync(emailAddress, unsubscribeLink);
|
||||
_logger.LogInformation("Confirmation email sent to {Email}", emailAddress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to send confirmation email to {Email}: {Error}", emailAddress, ex.Message);
|
||||
}
|
||||
|
||||
Response.StatusCode = 201;
|
||||
return new JsonResult(new { message = "Email has been added" });
|
||||
}
|
||||
}
|
||||
7
Pages/Unsubscribe.cshtml
Normal file
7
Pages/Unsubscribe.cshtml
Normal file
@@ -0,0 +1,7 @@
|
||||
@page
|
||||
@model UnsubscribeModel
|
||||
@{
|
||||
ViewData["Title"] = "Unsubscribe";
|
||||
Layout = null;
|
||||
}
|
||||
@Model.ResultMessage
|
||||
45
Pages/Unsubscribe.cshtml.cs
Normal file
45
Pages/Unsubscribe.cshtml.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using landing.Data;
|
||||
|
||||
namespace landing.Pages;
|
||||
|
||||
public class UnsubscribeModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ILogger<UnsubscribeModel> _logger;
|
||||
|
||||
public UnsubscribeModel(AppDbContext db, ILogger<UnsubscribeModel> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string ResultMessage { get; set; } = string.Empty;
|
||||
|
||||
public async Task<IActionResult> OnGetAsync([FromQuery] string? email)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
ResultMessage = "No email specified";
|
||||
return Page();
|
||||
}
|
||||
|
||||
var subscriber = await _db.Subscribers.FirstOrDefaultAsync(s => s.Email == email);
|
||||
if (subscriber == null)
|
||||
{
|
||||
Response.StatusCode = 400;
|
||||
ResultMessage = $"Email {email} was not found or already unsubscribed";
|
||||
return Page();
|
||||
}
|
||||
|
||||
_db.Subscribers.Remove(subscriber);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Unsubscribed {Email}", email);
|
||||
ResultMessage = $"The email {email} has been unsubscribed.";
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
4
Pages/_ViewImports.cshtml
Normal file
4
Pages/_ViewImports.cshtml
Normal file
@@ -0,0 +1,4 @@
|
||||
@using landing
|
||||
@using landing.Models
|
||||
@namespace landing.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
3
Pages/_ViewStart.cshtml
Normal file
3
Pages/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
Reference in New Issue
Block a user