rewrite number whatever to .net and blazor.

This commit is contained in:
Blake Ridgway
2026-02-08 21:05:52 -06:00
parent 3f7814d9c8
commit 6ae71b0216
51 changed files with 4531 additions and 1547 deletions

310
Pages/About.cshtml Normal file
View 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 &amp; 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">&infin;</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
View 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
View 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
View 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
View 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
View 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
View 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 &amp; 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 &amp; 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
View File

@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace landing.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}

View 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>
}

View 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
View 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>

View 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
View 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>&copy; 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
View File

@@ -0,0 +1,2 @@
@page
@model SubscribeModel

76
Pages/Subscribe.cshtml.cs Normal file
View 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
View File

@@ -0,0 +1,7 @@
@page
@model UnsubscribeModel
@{
ViewData["Title"] = "Unsubscribe";
Layout = null;
}
@Model.ResultMessage

View 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();
}
}

View File

@@ -0,0 +1,4 @@
@using landing
@using landing.Models
@namespace landing.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

3
Pages/_ViewStart.cshtml Normal file
View File

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}