diff --git a/CatherineLynwood/CatherineLynwood.csproj b/CatherineLynwood/CatherineLynwood.csproj index 8922008..ca80d57 100644 --- a/CatherineLynwood/CatherineLynwood.csproj +++ b/CatherineLynwood/CatherineLynwood.csproj @@ -160,7 +160,7 @@ - + diff --git a/CatherineLynwood/Controllers/DiscoveryController.cs b/CatherineLynwood/Controllers/DiscoveryController.cs index 3caaf7a..c10292f 100644 --- a/CatherineLynwood/Controllers/DiscoveryController.cs +++ b/CatherineLynwood/Controllers/DiscoveryController.cs @@ -62,6 +62,12 @@ namespace CatherineLynwood.Controllers return View(); } + [Route("how-to-buy")] + public IActionResult HowToBuy() + { + return View(); + } + [Route("")] public async Task Index() { @@ -71,15 +77,6 @@ namespace CatherineLynwood.Controllers return View(reviews); } - [Route("reviews")] - public async Task Reviews() - { - Reviews reviews = await _dataAccess.GetReviewsAsync(); - reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100); - - return View(reviews); - } - [BookAccess(1, 1)] [Route("extras/listen")] public IActionResult Listen() @@ -94,6 +91,15 @@ namespace CatherineLynwood.Controllers return View(); } + [Route("reviews")] + public async Task Reviews() + { + Reviews reviews = await _dataAccess.GetReviewsAsync(); + reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100); + + return View(reviews); + } + [BookAccess(1, 1)] [Route("extras/scrap-book")] public IActionResult ScrapBook() @@ -101,6 +107,14 @@ namespace CatherineLynwood.Controllers return View(); } + [Route("trailer")] + public async Task Trailer() + { + FlagSupportViewModel flagSupportViewModel = await _dataAccess.GetTrailerLikes(); + + return View(flagSupportViewModel); + } + #endregion Public Methods #region Private Methods @@ -245,8 +259,7 @@ namespace CatherineLynwood.Controllers return JsonConvert.SerializeObject(schema, Formatting.Indented); } - string StripHtml(string input) => string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty); - + private string StripHtml(string input) => string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty); #endregion Private Methods } diff --git a/CatherineLynwood/Controllers/HomeController.cs b/CatherineLynwood/Controllers/HomeController.cs index e3b586a..3effcf1 100644 --- a/CatherineLynwood/Controllers/HomeController.cs +++ b/CatherineLynwood/Controllers/HomeController.cs @@ -1,4 +1,4 @@ -using CatherineLynwood.Models; +using CatherineLynwood.Models; using CatherineLynwood.Services; using Microsoft.AspNetCore.Mvc; @@ -40,16 +40,39 @@ namespace CatherineLynwood.Controllers return View(arcReaderApplicationModel); } + [HttpGet("arc-reader-application/thanks")] + public IActionResult ArcThanks() + { + return View(); + } + [HttpPost("arc-reader-application")] + [ValidateAntiForgeryToken] public async Task ArcReaderApplication(ArcReaderApplicationModel arcReaderApplicationModel) { + if (!ModelState.IsValid) + { + return View(arcReaderApplicationModel); + } + bool success = await _dataAccess.SaveARCReaderApplication(arcReaderApplicationModel); if (success) { - return RedirectToAction("ThankYou"); + if (arcReaderApplicationModel.HasKindleAccess == "No") + { + await SendARCAltDeliveryEmailAsync(arcReaderApplicationModel); + } + else + { + await SendARCInstructionsEmailAsync(arcReaderApplicationModel); + } + + return RedirectToAction("ArcThanks"); } - + + // If saving fails unexpectedly + ModelState.AddModelError("", "There was an error submitting your application. Please try again later."); return View(arcReaderApplicationModel); } @@ -76,7 +99,7 @@ namespace CatherineLynwood.Controllers { if (ModelState.IsValid) { - await _dataAccess.SaveContact(contact); + await _dataAccess.SaveContact(contact, false); var subject = "Email from Catherine Lynwood Web Site"; var plainTextContent = $"Email from: {contact.Name} ({contact.EmailAddress})\r\n{contact.Message}"; @@ -193,8 +216,13 @@ namespace CatherineLynwood.Controllers } [Route("verostic-genre")] - public IActionResult VerosticGenre() + public IActionResult VerosticGenre(int step) { + if (step > 0) + { + ViewData["step"] = step; + } + return View(); } @@ -231,6 +259,83 @@ namespace CatherineLynwood.Controllers userAgent.Contains("ipad"); } + public async Task SendARCInstructionsEmailAsync(ArcReaderApplicationModel arc) + { + var subject = "Your ARC Copy of *The Alpha Flame: Discovery* – Kindle Instructions"; + + var plainTextContent = $@"Hi {arc.FullName}, + + Thank you so much for applying to be an ARC reader! I’m thrilled to have you on board. + + Since you selected Kindle delivery, here’s what you need to do next: + + 1. Add my sender email address to your approved Kindle senders list: catherine@catherinelynwood.com + 2. Make sure you’ve added your correct Kindle email: {arc.KindleEmail}@kindle.com + + You can do this by visiting https://www.amazon.co.uk/myk → Preferences → Personal Document Settings. + + Once this is done let me know and then I’ll be sending your ARC shortly! + + All my best, + Catherine"; + + var htmlContent = $@" +

Hi {arc.FullName},

+

Thank you so much for applying to be an ARC reader! I’m thrilled to have you on board.

+

Since you selected Kindle delivery, here’s what you need to do next:

+
    +
  1. Add my sender email address to your approved Kindle senders list: catherine@catherinelynwood.com
  2. +
  3. Ensure your Kindle email is correct: {arc.KindleEmail}@kindle.com
  4. +
+

You can find these settings at amazon.co.uk/myk → Preferences → Personal Document Settings.

+

Once that’s set up, let me know and I’ll send your ARC soon!

+

Warmly,
Catherine

"; + + Contact contact = new Contact + { + Name = arc.FullName, + EmailAddress = arc.Email + }; + + await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact, true); + } + + public async Task SendARCAltDeliveryEmailAsync(ArcReaderApplicationModel arc) + { + var subject = "Your ARC Copy of *The Alpha Flame: Discovery* – Next Steps"; + + var plainTextContent = $@"Hi {arc.FullName}, + + Thank you for applying to be an ARC reader – I really appreciate it! + + I noticed you selected an alternative to Kindle. That’s absolutely fine. + + I’ll be in touch shortly to offer a secure way to get your copy – one that’s simple and keeps the book protected from piracy. + + If you have any questions or preferred reading methods (like phone, tablet, computer), feel free to reply to this email. + + Thank you again, + Catherine"; + + var htmlContent = $@" +

Hi {arc.FullName},

+

Thank you for applying to be an ARC reader – I really appreciate it!

+

I noticed you selected an alternative to Kindle. That’s absolutely fine.

+

I’ll be in touch shortly to offer a secure way to get your copy – one that’s simple and keeps the book protected from piracy.

+

If you have any questions or preferred reading methods (like phone, tablet, or computer), just reply to this email.

+

Thanks again,
Catherine

"; + + Contact contact = new Contact + { + Name = arc.FullName, + EmailAddress = arc.Email + }; + + await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact, true); + } + + + #endregion Private Methods } } \ No newline at end of file diff --git a/CatherineLynwood/Controllers/SitemapController.cs b/CatherineLynwood/Controllers/SitemapController.cs index af59dc5..d738c55 100644 --- a/CatherineLynwood/Controllers/SitemapController.cs +++ b/CatherineLynwood/Controllers/SitemapController.cs @@ -38,16 +38,18 @@ namespace CatherineLynwood.Controllers new SitemapEntry { Url = Url.Action("Chapter1", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Chapter13", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Chapter2", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, - new SitemapEntry { Url = Url.Action("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("Trailer", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Characters", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("ContactCatherine", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Giveaways", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("HowToBuy", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "AskAQuestion", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "Publishing", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("Privacy", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, + new SitemapEntry { Url = Url.Action("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("SamanthaLynwood", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, new SitemapEntry { Url = Url.Action("VerosticGenre", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, // Additional static pages diff --git a/CatherineLynwood/Controllers/SupportController.cs b/CatherineLynwood/Controllers/SupportController.cs new file mode 100644 index 0000000..e53b259 --- /dev/null +++ b/CatherineLynwood/Controllers/SupportController.cs @@ -0,0 +1,68 @@ +using CatherineLynwood.Models; +using CatherineLynwood.Services; + +using Microsoft.AspNetCore.Mvc; + +using System.Threading.Tasks; + +namespace CatherineLynwood.Controllers +{ + + [ApiController] + [Route("api/support")] + public class SupportController : ControllerBase + { + private DataAccess _dataAccess; + + public record FlagDto(string Country); + + public class SubscriptionDto + { + public string Name { get; set; } + public string Email { get; set; } + } + + + public SupportController(DataAccess dataAccess) + { + _dataAccess = dataAccess; + } + + [HttpPost("flag")] + public async Task Flag([FromBody] FlagDto dto) + { + if (string.IsNullOrWhiteSpace(dto?.Country)) + return BadRequest(new { ok = false, error = "Country required" }); + + // TODO: replace with real DB call + var total = await _dataAccess.SaveFlagClick(dto.Country); + + return Ok(new { ok = true, total }); + } + + [HttpPost("subscribe")] + public IActionResult Subscribe([FromBody] SubscriptionDto dto) + { + if (string.IsNullOrWhiteSpace(dto?.Email)) + return BadRequest(new { ok = false, error = "Email required" }); + + Contact contact = new Contact + { + EmailAddress = dto.Email, + Name = dto.Name + }; + + var ok = _dataAccess.SaveContact(contact, true); + + return Ok(new { ok }); + } + + // ----- Dummy persistence you can swap for EF/Dapper calls ----- + + + private static bool SaveSubscription(string email, string country) + { + return true; + } + } +} diff --git a/CatherineLynwood/Controllers/TheAlphaFlameController.cs b/CatherineLynwood/Controllers/TheAlphaFlameController.cs index b3278b6..f06f3db 100644 --- a/CatherineLynwood/Controllers/TheAlphaFlameController.cs +++ b/CatherineLynwood/Controllers/TheAlphaFlameController.cs @@ -52,6 +52,14 @@ namespace CatherineLynwood.Controllers blogIndex.BlogFilter.TotalPages = (int)Math.Ceiling((double)blogIndex.Blogs.Count / blogFilter.ResultsPerPage); blogIndex.IsMobile = IsMobile(Request); + // Add this check before paginating + if (blogIndex.BlogFilter.PageNumber > blogIndex.BlogFilter.TotalPages && blogIndex.BlogFilter.TotalPages > 0) + { + Response.StatusCode = 404; + return View("BlogNotFound"); + + } + // Apply sorting if (blogFilter.SortDirection == 1) blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList(); diff --git a/CatherineLynwood/Middleware/SpamAndSecurityMiddleware.cs b/CatherineLynwood/Middleware/SpamAndSecurityMiddleware.cs index fb1a85e..9c60034 100644 --- a/CatherineLynwood/Middleware/SpamAndSecurityMiddleware.cs +++ b/CatherineLynwood/Middleware/SpamAndSecurityMiddleware.cs @@ -61,10 +61,10 @@ namespace CatherineLynwood.Middleware // if (path.EndsWith(".php", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".env", StringComparison.OrdinalIgnoreCase)) { - //if (!_environment.IsDevelopment() && ipAddress != null) - //{ - // TryBlockIpInIIS(ipAddress); - //} + if (!_environment.IsDevelopment() && ipAddress != null) + { + TryBlockIpInIIS(ipAddress); + } _logger.LogWarning("Blocked .php/.env probe from {IP}: {Path}", ipAddress, path); @@ -98,10 +98,10 @@ namespace CatherineLynwood.Middleware // Only block if referer is present but NOT in the whitelist if (!string.IsNullOrEmpty(referer) && !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.Ordinal))) { - //if (!_environment.IsDevelopment() && ipAddress != null) - //{ - // TryBlockIpInIIS(ipAddress); - //} + if (!_environment.IsDevelopment() && ipAddress != null) + { + TryBlockIpInIIS(ipAddress); + } _logger.LogWarning("Blocked POST with invalid referer: {Referer} from IP {IP}", referer, ipAddress); diff --git a/CatherineLynwood/Models/ARCReaderApplication.cs b/CatherineLynwood/Models/ARCReaderApplication.cs index 3a97c72..6e9dbfb 100644 --- a/CatherineLynwood/Models/ARCReaderApplication.cs +++ b/CatherineLynwood/Models/ARCReaderApplication.cs @@ -6,9 +6,6 @@ namespace CatherineLynwood.Models { #region Public Properties - [Required, StringLength(50)] - public string ApprovedSender { get; set; } - [Required, StringLength(50)] public string ContentFit { get; set; } @@ -20,6 +17,9 @@ namespace CatherineLynwood.Models [Required, StringLength(100)] public string FullName { get; set; } + [Required, StringLength(50)] + public string HasKindleAccess { get; set; } + [Key] public int Id { get; set; } diff --git a/CatherineLynwood/Models/ArcReaderApplicationModel.cs b/CatherineLynwood/Models/ArcReaderApplicationModel.cs index 02c5263..d0bea29 100644 --- a/CatherineLynwood/Models/ArcReaderApplicationModel.cs +++ b/CatherineLynwood/Models/ArcReaderApplicationModel.cs @@ -4,8 +4,8 @@ namespace CatherineLynwood.Models { public class ArcReaderApplicationModel { - [Required(ErrorMessage = "Please enter your full name.")] - [Display(Name = "Your Full Name", Prompt = "e.g. Catherine Lynwood")] + [Required(ErrorMessage = "Please enter your name.")] + [Display(Name = "Your Name", Prompt = "e.g. Catherine")] [StringLength(100, ErrorMessage = "Name must be under 100 characters.")] public string FullName { get; set; } @@ -15,13 +15,9 @@ namespace CatherineLynwood.Models [DataType(DataType.EmailAddress)] public string Email { get; set; } - [Required(ErrorMessage = "Please enter your Kindle email address.")] - [Display(Name = "Kindle Email Address", Prompt = "e.g. yourname")] - public string KindleEmail { get; set; } - - [Required(ErrorMessage = "Please select whether you've approved the sender.")] - [Display(Name = "Have you added my sender email to your Approved Senders list?")] - public string ApprovedSender { get; set; } + [Display(Name = "Your Kindle Email", Prompt = "e.g. yourname@kindle.com")] + [EmailAddress(ErrorMessage = "Please enter a valid Kindle email address.")] + public string? KindleEmail { get; set; } // Now optional [Display(Name = "Where do you plan to post your review?")] public List Platforms { get; set; } @@ -50,5 +46,10 @@ namespace CatherineLynwood.Models [DataType(DataType.MultilineText)] [StringLength(1000, ErrorMessage = "Please keep this under 1000 characters.")] public string? ExtraNotes { get; set; } + + [Required(ErrorMessage = "Please indicate whether you can receive the ARC via Kindle.")] + [Display(Name = "Do you have Kindle access?")] + public string HasKindleAccess { get; set; } // Expected values: "Yes" or "No" + } } diff --git a/CatherineLynwood/Models/FlagSupportViewModel.cs b/CatherineLynwood/Models/FlagSupportViewModel.cs new file mode 100644 index 0000000..c200409 --- /dev/null +++ b/CatherineLynwood/Models/FlagSupportViewModel.cs @@ -0,0 +1,8 @@ +namespace CatherineLynwood.Models +{ + public class FlagSupportViewModel + { + public Dictionary FlagCounts { get; set; } = new(); + } + +} diff --git a/CatherineLynwood/Services/DataAccess.cs b/CatherineLynwood/Services/DataAccess.cs index 69fd4bf..2f71ac9 100644 --- a/CatherineLynwood/Services/DataAccess.cs +++ b/CatherineLynwood/Services/DataAccess.cs @@ -97,6 +97,76 @@ namespace CatherineLynwood.Services return success; } + public async Task SaveFlagClick(string country) + { + int likes = 0; + + using (SqlConnection conn = new SqlConnection(_connectionString)) + { + using (SqlCommand cmd = new SqlCommand()) + { + try + { + await conn.OpenAsync(); + cmd.Connection = conn; + cmd.CommandType = CommandType.StoredProcedure; + cmd.CommandText = "SaveTrailerLike"; + cmd.Parameters.AddWithValue("@Country", country); + + using (SqlDataReader rdr = await cmd.ExecuteReaderAsync()) + { + while (await rdr.ReadAsync()) + { + likes = GetDataInt(rdr, "Likes"); + } + } + } + catch (Exception ex) + { + + } + } + } + + return likes; + } + + public async Task GetTrailerLikes() + { + FlagSupportViewModel flagSupportViewModel = new FlagSupportViewModel(); + + using (SqlConnection conn = new SqlConnection(_connectionString)) + { + using (SqlCommand cmd = new SqlCommand()) + { + try + { + await conn.OpenAsync(); + cmd.Connection = conn; + cmd.CommandType = CommandType.StoredProcedure; + cmd.CommandText = "GetTrailerLikes"; + + using (SqlDataReader rdr = await cmd.ExecuteReaderAsync()) + { + while (await rdr.ReadAsync()) + { + string country = GetDataString(rdr, "Country"); + int likes = GetDataInt(rdr, "Likes"); + + flagSupportViewModel.FlagCounts.Add(country, likes); + } + } + } + catch (Exception ex) + { + + } + } + } + + return flagSupportViewModel; + } + public async Task AddMarketingAsync(Marketing marketing) { bool success = true; @@ -303,7 +373,7 @@ namespace CatherineLynwood.Services { arcReaderList.Applications.Add(new ARCReaderApplication { - ApprovedSender = GetDataString(rdr, "ApprovedSender"), + HasKindleAccess = GetDataString(rdr, "HasKindleAccess"), ContentFit = GetDataString(rdr, "ContentFit"), Email = GetDataString(rdr, "Email"), ExtraNotes = GetDataString(rdr, "ExtraNotes"), @@ -765,7 +835,7 @@ namespace CatherineLynwood.Services return success; } - public async Task SaveContact(Contact contact) + public async Task SaveContact(Contact contact, bool subscribe = false) { bool success = true; @@ -781,10 +851,12 @@ namespace CatherineLynwood.Services cmd.CommandText = "SaveContact"; cmd.Parameters.AddWithValue("@Name", contact.Name); cmd.Parameters.AddWithValue("@EmailAddress", contact.EmailAddress); + cmd.Parameters.AddWithValue("@Subscribe", subscribe); await cmd.ExecuteNonQueryAsync(); } catch (Exception ex) { + success = false; } } } @@ -806,10 +878,12 @@ namespace CatherineLynwood.Services cmd.Connection = conn; cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = "SaveARCReader"; + cmd.Parameters.AddWithValue("@FullName", arcReaderApplication.FullName); cmd.Parameters.AddWithValue("@Email", arcReaderApplication.Email); - cmd.Parameters.AddWithValue("@KindleEmail", arcReaderApplication.KindleEmail); - cmd.Parameters.AddWithValue("@ApprovedSender", arcReaderApplication.ApprovedSender ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@KindleEmail", string.IsNullOrWhiteSpace(arcReaderApplication.KindleEmail) ? (object)DBNull.Value : arcReaderApplication.KindleEmail); + cmd.Parameters.AddWithValue("@HasKindleAccess", arcReaderApplication.HasKindleAccess); + cmd.Parameters.AddWithValue("@Platforms", arcReaderApplication.Platforms != null ? string.Join(",", arcReaderApplication.Platforms) : (object)DBNull.Value); cmd.Parameters.AddWithValue("@PreviewChapters", arcReaderApplication.PreviewChapters != null ? string.Join(",", arcReaderApplication.PreviewChapters) : (object)DBNull.Value); cmd.Parameters.AddWithValue("@PlatformsOther", string.IsNullOrWhiteSpace(arcReaderApplication.PlatformsOther) ? (object)DBNull.Value : arcReaderApplication.PlatformsOther); @@ -832,6 +906,7 @@ namespace CatherineLynwood.Services } + public async Task UpdateBlogAsync(Blog blog) { bool success = true; diff --git a/CatherineLynwood/Services/SmtpEmailService.cs b/CatherineLynwood/Services/SmtpEmailService.cs index 3e90cf7..491b59c 100644 --- a/CatherineLynwood/Services/SmtpEmailService.cs +++ b/CatherineLynwood/Services/SmtpEmailService.cs @@ -12,7 +12,7 @@ namespace CatherineLynwood.Services { public interface IEmailService { - Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact); + Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact, bool sendToUser = false); } public class SmtpEmailService : IEmailService @@ -24,12 +24,22 @@ namespace CatherineLynwood.Services _config = config; } - public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact) + public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact, bool sendToUser = false) { var message = new MimeMessage(); message.From.Add(new MailboxAddress("Catherine Lynwood", _config["Smtp:Sender"])); - message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com")); - message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress)); + + if (sendToUser) + { + message.To.Add(new MailboxAddress(contact.Name, contact.EmailAddress)); + message.Bcc.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com")); + } + else + { + message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com")); + message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress)); + } + message.Subject = subject; diff --git a/CatherineLynwood/Views/Admin/ArcReaders.cshtml b/CatherineLynwood/Views/Admin/ArcReaders.cshtml index 08e89d3..1b2451a 100644 --- a/CatherineLynwood/Views/Admin/ArcReaders.cshtml +++ b/CatherineLynwood/Views/Admin/ArcReaders.cshtml @@ -35,7 +35,7 @@

Email: @item.Email

Kindle Email: @item.KindleEmail

-

Approved Sender: @item.ApprovedSender

+

Has Kindle Access: @item.HasKindleAccess

Platforms: @item.Platforms

@if (!string.IsNullOrWhiteSpace(item.PlatformsOther)) { diff --git a/CatherineLynwood/Views/Discovery/HowToBuy.cshtml b/CatherineLynwood/Views/Discovery/HowToBuy.cshtml new file mode 100644 index 0000000..ad311ad --- /dev/null +++ b/CatherineLynwood/Views/Discovery/HowToBuy.cshtml @@ -0,0 +1,152 @@ +@{ + ViewData["Title"] = "How to Buy The Alpha Flame: Discovery"; + +} + +
+
+
+ +
+
+ +

How to Buy The Alpha Flame: Discovery

+ +

There are several ways to enjoy the book — whether you prefer digital, print, or audio. If you'd like to support the author directly, the direct links below are the best way to do so.

+ + +
+
+ eBook (Kindle) +
+
+

The Kindle edition is available via your local Amazon store:

+ + Buy Kindle Edition + +

Automatically redirects based on your country.

+
+
+ + +
+
+ Paperback (Bookshop Edition) +
+
+

+ This version is designed for local bookstores and global retailers. +

+ + Buy on Amazon + + + + + 📦 Buy Direct (Save & Support Author) + + +

ISBN 978-1-0682258-1-9

+

+
+
+ + +
+
+ Collector’s Edition (Hardback) +
+
+

+ A premium collector’s hardback edition, available via bookstores and online. +

+ + Buy on Amazon + + + + + 💎 Buy Direct (Save & Support Author) + + +

ISBN 978-1-0682258-0-2

+

+
+
+ + +
+
+ Audiobook (AI-Read) +
+
+

Listen to the entire book for free on ElevenLabs:

+ + 🎧 Listen on ElevenLabs + +
+ + Scan to listen +
+
+
+ + +@section Scripts{ + + + +} + diff --git a/CatherineLynwood/Views/Discovery/Index.cshtml b/CatherineLynwood/Views/Discovery/Index.cshtml index 6979394..dc2726c 100644 --- a/CatherineLynwood/Views/Discovery/Index.cshtml +++ b/CatherineLynwood/Views/Discovery/Index.cshtml @@ -44,29 +44,20 @@

Survival, secrets, and sisters in 1980s Birmingham.

+

Buy the Book

-
Buy Kindle Edition - - Buy Paperback (Bookshop Edition) - - - Buy Hardback (Collector's Edition) - -

- Available from your local Amazon store.
- Or order from your local bookshop using: -

    -
  • ISBN 978-1-0682258-1-9 – Bookshop Edition (Paperback)
  • -
  • ISBN 978-1-0682258-0-2 – Collector's Edition (Hardback)
  • -
- +

+ Want paperback, hardback, or audiobook options?

+ + See All Buying Options +
diff --git a/CatherineLynwood/Views/Discovery/Trailer.cshtml b/CatherineLynwood/Views/Discovery/Trailer.cshtml new file mode 100644 index 0000000..b3782a4 --- /dev/null +++ b/CatherineLynwood/Views/Discovery/Trailer.cshtml @@ -0,0 +1,308 @@ +@model CatherineLynwood.Models.FlagSupportViewModel + +@{ + ViewData["Title"] = "The Alpha Flame - Coming Soon"; +} + + +
+
+
+ +
+

The Alpha Flame: Discovery

+

A gritty Birmingham crime novel set in 1983

+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+

Show your support

+

Tap your flag to show your support.

+
+
+
+ @foreach (var kv in Model.FlagCounts) + { + var code = kv.Key; + var count = kv.Value; + var name = code switch + { + "UK" => "UK", + "US" => "US", + "CA" => "Canada", + "AU" => "Australia", + "IE" => "Ireland", + "NZ" => "New Zealand", + _ => code + }; + var flagFile = code.ToLower() switch + { + "uk" => "gb", + _ => code.ToLower() + }; + + } +
+ + + +
+
+
+ + + + + +
+
+
+

A glimpse inside

+
+ +
+
+
+
+
+

“You looking for something, love?” she asked, her voice soft but direct. Her lips were parted just slightly, her breath misting against the cold window.

+
+ +
+
+
+ +
+
+
+
+
+
+
+

“Maggie…” My voice broke. “It’s hers. She used to wear this all the time. She was wearing it the last time I saw her.”

+
+ +
+
+
+ +
+
+ +
+
+
+
+
+

Waves erased our footprints; morning would come. So would he.

+
+ +
+
+
+ +
+
+
+
+ + +
+
+
+

Coming 21st August 2025 to major retailers.

+ +
+
+
+ + + + +@* + + *@ +@section Scripts { + + + + + + + + +} diff --git a/CatherineLynwood/Views/Home/ARCThanks.cshtml b/CatherineLynwood/Views/Home/ARCThanks.cshtml new file mode 100644 index 0000000..8e4d310 --- /dev/null +++ b/CatherineLynwood/Views/Home/ARCThanks.cshtml @@ -0,0 +1,28 @@ +@{ + ViewData["Title"] = "Thank You"; +} + +
+
+
+

Thank You for Applying!

+

I'm so grateful you've offered to read The Alpha Flame: Discovery and consider reviewing it. It really means a lot.

+ +
+
📩 Please check your inbox
+

I've just sent you an email with setup instructions based on how you said you'd like to receive the book — whether via Kindle or an alternative method.

+

If it doesn't appear soon, please check your spam or junk folder. If it’s not there either, feel free to contact me directly.

+
+ +
+
✅ One last step...
+

Once you've completed the setup (or if you need help), please reply to the email and let me know. That way I can get the ARC sent out to you right away.

+
+ +

Thanks again for your support — it genuinely makes a difference.

+

Warmest wishes,
Catherine

+ + Return to Home Page +
+
+
diff --git a/CatherineLynwood/Views/Home/ArcReaderApplication.cshtml b/CatherineLynwood/Views/Home/ArcReaderApplication.cshtml index cdea641..67357d4 100644 --- a/CatherineLynwood/Views/Home/ArcReaderApplication.cshtml +++ b/CatherineLynwood/Views/Home/ArcReaderApplication.cshtml @@ -10,7 +10,7 @@

For The Alpha Flame: Discovery – Advance Reader Copy (ARC)

-
+
Step 1 of 5 @@ -23,34 +23,31 @@

Become an ARC Reader

Fancy reading The Alpha Flame: Discovery before anyone else? I'm looking for passionate early readers to receive a free Kindle copy in exchange for an honest review.

+ -
- - Note: This novel is raw and emotional. Please check the themes below. -
-

Major Themes and Topics

-
    -
  • Death, trauma, and grief through the eyes of a teenage girl
  • -
  • Physical and sexual abuse (non-graphic, but deeply affecting)
  • -
  • Mental health, including suicidal thoughts and PTSD
  • -
  • Prostitution, exploitation, and coercion
  • -
  • Violence against women (threats, assaults, murder)
  • -
  • Love, trust, tenderness, and emotional recovery
  • -
  • Sexuality, orientation, and identity discovery
  • -
  • Found family, sisterhood, and female empowerment
  • -
  • Corruption, manipulation, and cover-ups
  • -
  • Justice, revenge, and difficult moral choices
  • -
  • Music and fashion as creative expression and survival
  • -
+

+ The Alpha Flame: Discovery is a gritty, character-driven crime novel set in 1983 Birmingham. Beth and Maggie are thrown together by fate, each carrying trauma, secrets, and fire. Beth is a survivor — wounded, wary, and haunted by the past. Maggie is bold, passionate, and dangerous to underestimate. Their bond is raw, explosive, and deeply human. Together, they must face a world that wants to break them — and fight back harder. +

+

+ This is a story of survival, sisterhood, and power. Unflinching in its honesty, The Alpha Flame explores abuse, love, identity, and the fragile strength that carries us through the darkest nights. With fast cars, dangerous men, and high-stakes emotion set against the electric backdrop of 1980s Britain, this is not a soft read — but it burns with hope, truth, and fierce female energy. +

+

+ If you'd like to know more then take a look at the main book page. +

+

+ Explore the Book +

If you’re still interested, I’d love to have you on board. ARC readers help spread the word and offer early feedback that matters.

All I ask: read the book and leave a review on Goodreads, Amazon, or wherever you normally post.

- +
- +
@@ -66,61 +63,56 @@
- This is where I’ll send updates and reminders. + This is where I’ll send updates and reminders, and generally keep in touch.
- +
-
+
-

ARCs are sent via Kindle only. You can use the free Kindle app or a Kindle device.

-

Add the following sender to your Amazon Kindle Approved Senders list: [enable JavaScript to view]

-

- To find your kindle email follow this link amazon.co.uk/myk. - Once there click on Preferences and then scroll down to Personal Document Settings, as shown in the screen shot. -

-
-
- -
- - @@kindle.com -
- +

How will you receive the ARC?

+

To protect the unpublished book from piracy, I send all ARC copies securely via Kindle. This sends the book directly to your Kindle app or device — no files to download, nothing to forward.

+

You don’t need a physical Kindle. The free Kindle app works on phones, tablets, and desktops.

+

Just let me know below if you have Kindle access. I’ll email you simple step-by-step instructions to get everything set up.

+
- +
- - + +
- - + +
-
- - -
- + +
+ + +
+ + + + If you know your Kindle email address already, pop it in here. Otherwise I’ll help you via email.
-
- -
+
- +
+ +
@@ -168,7 +160,7 @@
- +
@@ -177,7 +169,10 @@
- I'm interested in the type of fiction you enjoy reading. I write what I describe as verostic fiction, I've even got a page on this website describing it. + I'm interested in the type of fiction you enjoy reading. + I write what I describe as verostic fiction, + it's A literary genre or descriptive tone characterised by raw emotional realism, unflinching psychological depth, and grounded human truth. + Often gritty, sometimes painful, but always sincere.
@@ -263,6 +258,16 @@ }); + + - } \ No newline at end of file diff --git a/CatherineLynwood/Views/Home/VerosticGenre.cshtml b/CatherineLynwood/Views/Home/VerosticGenre.cshtml index 798e107..46d74c5 100644 --- a/CatherineLynwood/Views/Home/VerosticGenre.cshtml +++ b/CatherineLynwood/Views/Home/VerosticGenre.cshtml @@ -13,6 +13,18 @@
+@if (ViewData["step"] != null) +{ + var step = ViewData["step"]; + +} +

The Verostic Genre

diff --git a/CatherineLynwood/Views/Shared/_Layout.cshtml b/CatherineLynwood/Views/Shared/_Layout.cshtml index c540ef4..2e6b184 100644 --- a/CatherineLynwood/Views/Shared/_Layout.cshtml +++ b/CatherineLynwood/Views/Shared/_Layout.cshtml @@ -74,8 +74,8 @@