diff --git a/CatherineLynwood/CatherineLynwood.csproj b/CatherineLynwood/CatherineLynwood.csproj index ca80d57..074322a 100644 --- a/CatherineLynwood/CatherineLynwood.csproj +++ b/CatherineLynwood/CatherineLynwood.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -154,18 +154,18 @@ - - + + - + - - - - + + + + - + diff --git a/CatherineLynwood/Components/BuyPanel.cs b/CatherineLynwood/Components/BuyPanel.cs new file mode 100644 index 0000000..4998bdc --- /dev/null +++ b/CatherineLynwood/Components/BuyPanel.cs @@ -0,0 +1,27 @@ +using CatherineLynwood.Models; +using CatherineLynwood.Services; + +using Microsoft.AspNetCore.Mvc; + +using Newtonsoft.Json; + +namespace CatherineLynwood.Components +{ + public class BuyPanel : ViewComponent + { + private DataAccess _dataAccess; + + public BuyPanel(DataAccess dataAccess) + { + _dataAccess = dataAccess; + } + + public async Task InvokeAsync(string iso2, string src, string title) + { + BuyPanelViewModel buyPanelViewModel = await _dataAccess.GetBuyPanelViewModel(iso2, title); + buyPanelViewModel.Src = src; + + return View(buyPanelViewModel); + } + } +} diff --git a/CatherineLynwood/Controllers/ClicksController.cs b/CatherineLynwood/Controllers/ClicksController.cs index 55696fc..f2c5756 100644 --- a/CatherineLynwood/Controllers/ClicksController.cs +++ b/CatherineLynwood/Controllers/ClicksController.cs @@ -3,317 +3,20 @@ using CatherineLynwood.Services; using Microsoft.AspNetCore.Mvc; +using System.Diagnostics.Metrics; + namespace CatherineLynwood.Controllers { [Route("")] public class ClicksController : Controller { private readonly DataAccess _dataAccess; + private readonly ICountryContext _country; - // Central map of slugs -> destination URLs and metadata - private static readonly Dictionary Links = new() - { - // --- Ingram direct (GB/US only) --- - ["ingram-hardback-gb"] = new BuyLink - { - Slug = "ingram-hardback-gb", - Url = "https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO", - Retailer = "Ingram", - Format = "Hardback", - CountryGroup = "GB" - }, - ["ingram-paperback-gb"] = new BuyLink - { - Slug = "ingram-paperback-gb", - Url = "https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ", - Retailer = "Ingram", - Format = "Paperback", - CountryGroup = "GB" - }, - ["ingram-hardback-us"] = new BuyLink - { - Slug = "ingram-hardback-us", - Url = "https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO", - Retailer = "Ingram", - Format = "Hardback", - CountryGroup = "US" - }, - ["ingram-paperback-us"] = new BuyLink - { - Slug = "ingram-paperback-us", - Url = "https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ", - Retailer = "Ingram", - Format = "Paperback", - CountryGroup = "US" - }, - - // --- Amazon (GB/US/CA/AU) --- - ["amazon-hardback-gb"] = new BuyLink - { - Slug = "amazon-hardback-gb", - Url = "https://www.amazon.co.uk/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "GB" - }, - ["amazon-paperback-gb"] = new BuyLink - { - Slug = "amazon-paperback-gb", - Url = "https://www.amazon.co.uk/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "GB" - }, - ["amazon-kindle-gb"] = new BuyLink - { - Slug = "amazon-kindle-gb", - Url = "https://www.amazon.co.uk/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "GB" - }, - - ["amazon-hardback-us"] = new BuyLink - { - Slug = "amazon-hardback-us", - Url = "https://www.amazon.com/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "US" - }, - ["amazon-paperback-us"] = new BuyLink - { - Slug = "amazon-paperback-us", - Url = "https://www.amazon.com/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "US" - }, - ["amazon-kindle-us"] = new BuyLink - { - Slug = "amazon-kindle-us", - Url = "https://www.amazon.com/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "US" - }, - - ["amazon-hardback-ca"] = new BuyLink - { - Slug = "amazon-hardback-ca", - Url = "https://www.amazon.ca/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "CA" - }, - ["amazon-paperback-ca"] = new BuyLink - { - Slug = "amazon-paperback-ca", - Url = "https://www.amazon.ca/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "CA" - }, - ["amazon-kindle-ca"] = new BuyLink - { - Slug = "amazon-kindle-ca", - Url = "https://www.amazon.ca/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "CA" - }, - - ["amazon-hardback-au"] = new BuyLink - { - Slug = "amazon-hardback-au", - Url = "https://www.amazon.com.au/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "AU" - }, - ["amazon-paperback-au"] = new BuyLink - { - Slug = "amazon-paperback-au", - Url = "https://www.amazon.com.au/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "AU" - }, - ["amazon-kindle-au"] = new BuyLink - { - Slug = "amazon-kindle-au", - Url = "https://www.amazon.com.au/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "AU" - }, - - // --- National retailers --- - ["waterstones-hardback-gb"] = new BuyLink - { - Slug = "waterstones-hardback-gb", - Url = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802", - Retailer = "Waterstones", - Format = "Hardback", - CountryGroup = "GB" - }, - ["waterstones-paperback-gb"] = new BuyLink - { - Slug = "waterstones-paperback-gb", - Url = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819", - Retailer = "Waterstones", - Format = "Paperback", - CountryGroup = "GB" - }, - - ["bn-hardback-us"] = new BuyLink - { - Slug = "bn-hardback-us", - Url = "https://www.barnesandnoble.com/s/9781068225802", - Retailer = "Barnes & Noble", - Format = "Hardback", - CountryGroup = "US" - }, - ["bn-paperback-us"] = new BuyLink - { - Slug = "bn-paperback-us", - Url = "https://www.barnesandnoble.com/s/9781068225819", - Retailer = "Barnes & Noble", - Format = "Paperback", - CountryGroup = "US" - }, - - // --- Apple Books (GB/US/CA/AU/IE) --- - ["apple-ebook-gb"] = new BuyLink - { - Slug = "apple-ebook-gb", - Url = "https://books.apple.com/gb/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "GB" - }, - ["apple-ebook-us"] = new BuyLink - { - Slug = "apple-ebook-us", - Url = "https://books.apple.com/us/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "US" - }, - ["apple-ebook-ca"] = new BuyLink - { - Slug = "apple-ebook-ca", - Url = "https://books.apple.com/ca/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "CA" - }, - ["apple-ebook-au"] = new BuyLink - { - Slug = "apple-ebook-au", - Url = "https://books.apple.com/au/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "AU" - }, - ["apple-ebook-ie"] = new BuyLink - { - Slug = "apple-ebook-ie", - Url = "https://books.apple.com/ie/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "IE" - }, - - // --- Kobo (GB/US/CA/AU/IE) --- - ["kobo-ebook-gb"] = new BuyLink - { - Slug = "kobo-ebook-gb", - Url = "https://www.kobo.com/gb/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "GB" - }, - ["kobo-ebook-us"] = new BuyLink - { - Slug = "kobo-ebook-us", - Url = "https://www.kobo.com/us/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "US" - }, - ["kobo-ebook-ca"] = new BuyLink - { - Slug = "kobo-ebook-ca", - Url = "https://www.kobo.com/ca/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "CA" - }, - ["kobo-ebook-au"] = new BuyLink - { - Slug = "kobo-ebook-au", - Url = "https://www.kobo.com/au/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "AU" - }, - ["kobo-ebook-ie"] = new BuyLink - { - Slug = "kobo-ebook-ie", - Url = "https://www.kobo.com/ie/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "IE" - } - }; - - public ClicksController(DataAccess dataAccess) + public ClicksController(DataAccess dataAccess, ICountryContext country) { _dataAccess = dataAccess; - } - - // --------------------------------------------------------------------- - // GET /go/{slug} -> logs the click server-side, then redirects - // --------------------------------------------------------------------- - [HttpGet("go/{slug}")] - public async Task Go(string slug) - { - if (!Links.TryGetValue(slug, out var link)) return NotFound(); - - var ua = Request.Headers["User-Agent"].ToString() ?? string.Empty; - var referer = Request.Headers["Referer"].ToString() ?? string.Empty; - var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); - var qs = Request.QueryString.HasValue ? Request.QueryString.Value : string.Empty; - - // Keep lightweight bot/protection checks - if (HttpMethods.IsHead(Request.Method)) return Redirect(link.Url); - if (IsLikelyBot(ua)) return Redirect(link.Url); - - var fetchMode = Request.Headers["Sec-Fetch-Mode"].ToString(); - if (!string.Equals(fetchMode, "navigate", StringComparison.OrdinalIgnoreCase)) - return Redirect(link.Url); - - EnsureSidCookie(out var sessionId); - - // Persist the click - _ = await _dataAccess.SaveBuyClick( - dateTimeUtc: DateTime.UtcNow, - slug: link.Slug, - retailer: link.Retailer, - format: link.Format, - countryGroup: link.CountryGroup, - ip: ip, - country: Request.Query["country"].ToString(), - userAgent: ua, - referer: referer, - page: referer, - sessionId: sessionId, - queryString: qs, - destinationUrl: link.Url - ); - - return Redirect(link.Url); + _country = country; } // --------------------------------------------------------------------- @@ -323,32 +26,34 @@ namespace CatherineLynwood.Controllers [HttpPost("track/click")] public async Task TrackClick([FromQuery] string slug, [FromQuery] string src = "discovery") { - if (string.IsNullOrWhiteSpace(slug)) return NoContent(); - - Links.TryGetValue(slug, out var link); - + bool logClick = true; var ua = Request.Headers["User-Agent"].ToString() ?? string.Empty; var referer = Request.Headers["Referer"].ToString() ?? string.Empty; var ip = HttpContext.Connection.RemoteIpAddress?.ToString(); var qs = Request.QueryString.HasValue ? Request.QueryString.Value : string.Empty; + var iso2 = (_country.Iso2 ?? "UK").ToUpperInvariant(); + if (iso2 == "GB") iso2 = "UK"; - EnsureSidCookie(out var sessionId); + if (HttpMethods.IsHead(Request.Method)) logClick = false; // don't log HEAD requests - _ = await _dataAccess.SaveBuyClick( - dateTimeUtc: DateTime.UtcNow, - slug: slug, - retailer: link?.Retailer ?? "", - format: link?.Format ?? "", - countryGroup: link?.CountryGroup ?? "", - ip: ip, - country: "", // optional for ping - userAgent: ua, - referer: referer, - page: src, - sessionId: sessionId, - queryString: qs, - destinationUrl: link?.Url ?? "" - ); + if (IsLikelyBot(ua)) logClick = false; // don't log likely bots + + if (logClick) + { + EnsureSidCookie(out var sessionId); + + _ = await _dataAccess.SaveBuyClick( + dateTimeUtc: DateTime.UtcNow, + slug: slug, + countryGroup: iso2, + ip: ip, + country: Request.Query["country"].ToString(), + userAgent: ua, + referer: referer, + sessionId: sessionId, + src: src + ); + } return NoContent(); } diff --git a/CatherineLynwood/Controllers/CollaborationsController.cs b/CatherineLynwood/Controllers/CollaborationsController.cs new file mode 100644 index 0000000..74684eb --- /dev/null +++ b/CatherineLynwood/Controllers/CollaborationsController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace CatherineLynwood.Controllers +{ + [Route("collaborations")] + public class CollaborationsController : Controller + { + public IActionResult Index() + { + return View(); + } + + [Route("larhysa-saddul")] + public IActionResult LarhysaSaddul() + { + return View(); + } + } +} diff --git a/CatherineLynwood/Controllers/DiscoveryController.cs b/CatherineLynwood/Controllers/DiscoveryController.cs index 5ec5228..13f0f6b 100644 --- a/CatherineLynwood/Controllers/DiscoveryController.cs +++ b/CatherineLynwood/Controllers/DiscoveryController.cs @@ -7,8 +7,6 @@ using Newtonsoft.Json; using System.Globalization; using System.Security.Cryptography; -using System.Text; -using System.Text.Json; using System.Text.RegularExpressions; namespace CatherineLynwood.Controllers @@ -45,28 +43,25 @@ namespace CatherineLynwood.Controllers var iso2 = (_country.Iso2 ?? "GB").ToUpperInvariant(); if (iso2 == "UK") iso2 = "GB"; - // Buy links - var buyLinks = BuildBuyLinksFor(iso2); - // Reviews - Reviews reviews = await _dataAccess.GetReviewsAsync(); + Reviews reviews = await _dataAccess.GetReviewsAsync("Discovery"); reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3); string src = variant switch { Variant.A => "DiscoveryA", - Variant.B => "DiscoveryB", - _ => "DiscoveryC" + Variant.B => "DiscoveryA", + _ => "Desktop" }; // VM - var vm = new DiscoveryPageViewModel + var vm = new TitlePageViewModel { Reviews = reviews, UserIso2 = iso2, - Buy = buyLinks, - Src = src + Src = src, + Title = "Discovery" }; // View mapping: @@ -75,7 +70,7 @@ namespace CatherineLynwood.Controllers string viewName = variant switch { Variant.A => "IndexMobileA", // mobile layout A - Variant.B => "IndexMobileB", // mobile layout B + Variant.B => "IndexMobileA", // mobile layout B _ => "IndexDesktop" // desktop layout C }; @@ -85,11 +80,14 @@ namespace CatherineLynwood.Controllers [Route("reviews")] public async Task Reviews() { - Reviews reviews = await _dataAccess.GetReviewsAsync(); + Reviews reviews = await _dataAccess.GetReviewsAsync("Discovery"); reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100); return View(reviews); } + [Route("audio-book")] + public IActionResult AudioBook() => View(); + [Route("how-to-buy")] public IActionResult HowToBuy() => View(); @@ -288,79 +286,6 @@ namespace CatherineLynwood.Controllers // Existing helpers // ========================= - private static BuyLinksViewModel BuildBuyLinksFor(string iso2) - { - var cc = iso2.ToUpperInvariant(); - if (cc == "UK") cc = "GB"; - - string? ingHbSlug = null, ingPbSlug = null; - if (cc == "GB") { ingHbSlug = "ingram-hardback-gb"; ingPbSlug = "ingram-paperback-gb"; } - else if (cc == "US") { ingHbSlug = "ingram-hardback-us"; ingPbSlug = "ingram-paperback-us"; } - - string amazonHbSlug = cc switch - { - "GB" => "amazon-hardback-gb", - "US" => "amazon-hardback-us", - "CA" => "amazon-hardback-ca", - "AU" => "amazon-hardback-au", - _ => "amazon-hardback-us" - }; - string amazonPbSlug = cc switch - { - "GB" => "amazon-paperback-gb", - "US" => "amazon-paperback-us", - "CA" => "amazon-paperback-ca", - "AU" => "amazon-paperback-au", - _ => "amazon-paperback-us" - }; - string amazonKindleSlug = cc switch - { - "GB" => "amazon-kindle-gb", - "US" => "amazon-kindle-us", - "CA" => "amazon-kindle-ca", - "AU" => "amazon-kindle-au", - _ => "amazon-kindle-us" - }; - - string? natHbSlug = null, natPbSlug = null, natLabel = null; - if (cc == "GB") { natHbSlug = "waterstones-hardback-gb"; natPbSlug = "waterstones-paperback-gb"; natLabel = "Waterstones"; } - else if (cc == "US") { natHbSlug = "bn-hardback-us"; natPbSlug = "bn-paperback-us"; natLabel = "B&N"; } - - var ccLower = cc.ToLowerInvariant(); - var appleSlug = ccLower is "gb" or "us" or "ca" or "au" or "ie" ? $"apple-ebook-{ccLower}" : "apple-ebook-gb"; - var koboSlug = ccLower is "gb" or "us" or "ca" or "au" or "ie" ? $"kobo-ebook-{ccLower}" : "kobo-ebook-gb"; - - LinkChoice? mk(string? slug) => string.IsNullOrEmpty(slug) ? null : new LinkChoice { Slug = slug, Url = BuyCatalog.Url(slug) }; - - var vm = new BuyLinksViewModel - { - Country = cc, - IngramHardback = mk(ingHbSlug), - IngramPaperback = mk(ingPbSlug), - AmazonHardback = mk(amazonHbSlug)!, - AmazonPaperback = mk(amazonPbSlug)!, - AmazonKindle = mk(amazonKindleSlug)!, - NationalHardback = mk(natHbSlug), - NationalPaperback = mk(natPbSlug), - NationalLabel = natLabel, - Apple = mk(appleSlug)!, - Kobo = mk(koboSlug)! - }; - - if (cc == "GB") - { - vm.IngramHardbackPrice = "£16.99"; - vm.IngramPaperbackPrice = "£12.99"; - } - else if (cc == "US") - { - vm.IngramHardbackPrice = "$24.99"; - vm.IngramPaperbackPrice = "$16.99"; - } - - return vm; - } - private string GenerateBookSchemaJsonLd(Reviews reviews, int take) { const string imageUrl = "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"; diff --git a/CatherineLynwood/Controllers/IndieAuthorController.cs b/CatherineLynwood/Controllers/IndieAuthorController.cs new file mode 100644 index 0000000..7644538 --- /dev/null +++ b/CatherineLynwood/Controllers/IndieAuthorController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace CatherineLynwood.Controllers +{ + public class IndieAuthorController : Controller + { + public IActionResult Index() + { + return View(); + } + } +} diff --git a/CatherineLynwood/Controllers/ReckoningController.cs b/CatherineLynwood/Controllers/ReckoningController.cs new file mode 100644 index 0000000..1df8602 --- /dev/null +++ b/CatherineLynwood/Controllers/ReckoningController.cs @@ -0,0 +1,399 @@ +using CatherineLynwood.Models; +using CatherineLynwood.Services; + +using Microsoft.AspNetCore.Mvc; + +using Newtonsoft.Json; + +using System.Globalization; +using System.Security.Cryptography; +using System.Text.RegularExpressions; + +namespace CatherineLynwood.Controllers +{ + [Route("the-alpha-flame/reckoning")] + public class ReckoningController : Controller + { + private readonly DataAccess _dataAccess; + private readonly ICountryContext _country; + + // A/B/C constants + private const string VariantCookie = "af_disc_abc"; + private const string VariantQuery = "ab"; // ?ab=A or B or C + + public ReckoningController(DataAccess dataAccess, ICountryContext country) + { + _dataAccess = dataAccess; + _country = country; + } + + [Route("")] + public async Task Index() + { + // Country ISO2 + var iso2 = (_country.Iso2 ?? "GB").ToUpperInvariant(); + if (iso2 == "UK") iso2 = "GB"; + + // Reviews + Reviews reviews = await _dataAccess.GetReviewsAsync("Reckoning"); + reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3); + + // VM + var vm = new TitlePageViewModel + { + Reviews = reviews, + UserIso2 = iso2, + Src = "Index", + Title = "Reckoning" + }; + + return View("Index", vm); + } + + [Route("reviews")] + public async Task Reviews() + { + Reviews reviews = await _dataAccess.GetReviewsAsync("Reckoning"); + reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100); + return View(reviews); + } + + [Route("how-to-buy")] + public IActionResult HowToBuy() => View(); + + [Route("chapters/chapter-1-beth")] + public IActionResult Chapter1() => View(); + + [Route("chapters/chapter-2-maggie")] + public IActionResult Chapter2() => View(); + + [Route("chapters/chapter-13-susie")] + public IActionResult Chapter13() => View(); + + [Route("trailer")] + public async Task Trailer() + { + // Country ISO2 + var iso2 = (_country.Iso2 ?? "UK").ToUpperInvariant(); + if (iso2 == "GB") iso2 = "UK"; + + FlagSupportViewModel flagSupportViewModel = await _dataAccess.GetTrailerLikes(); + + foreach (var flag in flagSupportViewModel.FlagCounts) + { + flag.Selected = flag.Key.ToLower() == iso2.ToLower(); + } + + return View(flagSupportViewModel); + } + + [BookAccess(1, 1)] + [Route("extras")] + public IActionResult Extras() => View(); + + [BookAccess(1, 1)] + [Route("extras/epilogue")] + public IActionResult Epilogue() => View(); + + [BookAccess(1, 1)] + [Route("extras/scrap-book")] + public IActionResult ScrapBook() => View(); + + [BookAccess(1, 1)] + [Route("extras/listen")] + public IActionResult Listen() => View(); + + [BookAccess(1, 1)] + [Route("extras/maggies-designs")] + public IActionResult MaggiesDesigns() => View(); + + [BookAccess(1, 1)] + [Route("extras/soundtrack")] + public async Task Soundtrack() + { + List soundtrackTrackModels = await _dataAccess.GetSoundtrack(); + return View(soundtrackTrackModels); + } + + // ========================= + // A/B/C selection + // ========================= + + private enum Variant { A, B, C } + private enum DeviceClass { Mobile, Desktop } + + private Variant ResolveVariant(DeviceClass device) + { + // 0) Query override: ?ab=A|B|C forces and persists + if (Request.Query.TryGetValue(VariantQuery, out var qs)) + { + var parsed = ParseVariant(qs.ToString()); + if (parsed.HasValue) + { + // If they force A or B but device is desktop, we still respect C logic by default. + // Allow override to win for testing convenience. + WriteVariantCookie(parsed.Value); + return parsed.Value; + } + } + + // 1) Existing cookie + if (Request.Cookies.TryGetValue(VariantCookie, out var cookieVal)) + { + var parsed = ParseVariant(cookieVal); + if (parsed.HasValue) + { + // If cookie says A/B but device is desktop, upgrade to C to keep desktop experience consistent. + if (device == DeviceClass.Desktop && parsed.Value != Variant.C) + { + WriteVariantCookie(Variant.C); + return Variant.C; + } + // If cookie says C but device is mobile, you can either keep C or reassign to A/B. + // We keep C only for desktops. On mobile we reassess to A/B the first time after cookie mismatch. + if (device == DeviceClass.Mobile && parsed.Value == Variant.C) + { + var reassigned = AssignMobileVariant(); + WriteVariantCookie(reassigned); + return reassigned; + } + return parsed.Value; + } + } + + // 2) Fresh assignment + if (device == DeviceClass.Desktop) + { + WriteVariantCookie(Variant.C); + return Variant.C; + } + else + { + var assigned = AssignMobileVariant(); // A or B + WriteVariantCookie(assigned); + return assigned; + } + } + + private static Variant AssignMobileVariant(int bPercent = 50) + { + bPercent = Math.Clamp(bPercent, 0, 100); + var roll = RandomNumberGenerator.GetInt32(0, 100); // 0..99 + return roll < bPercent ? Variant.B : Variant.A; + } + + private static Variant? ParseVariant(string value) + { + if (string.Equals(value, "A", StringComparison.OrdinalIgnoreCase)) return Variant.A; + if (string.Equals(value, "B", StringComparison.OrdinalIgnoreCase)) return Variant.B; + if (string.Equals(value, "C", StringComparison.OrdinalIgnoreCase)) return Variant.C; + return null; + } + + private void WriteVariantCookie(Variant variant) + { + var opts = new CookieOptions + { + Expires = DateTimeOffset.UtcNow.AddDays(90), + HttpOnly = false, // set true if you do not need analytics to read it + Secure = true, + SameSite = SameSiteMode.Lax, + Path = "/" + }; + Response.Cookies.Append(VariantCookie, variant.ToString(), opts); + } + + // ========================= + // Device detection + // ========================= + private DeviceClass ResolveDeviceClass() + { + // Simple and robust server-side approach using User-Agent. + // Chrome UA reduction still leaves Mobile hint for Android; iOS strings include iPhone/iPad. + var ua = Request.Headers.UserAgent.ToString(); + + if (IsMobileUserAgent(ua)) + return DeviceClass.Mobile; + + return DeviceClass.Desktop; + } + + private static bool IsMobileUserAgent(string ua) + { + if (string.IsNullOrEmpty(ua)) return false; + + // Common mobile indicators + // Android phones include "Android" and "Mobile" + // iPhone includes "iPhone"; iPad includes "iPad" (treat as mobile for your layouts) + // Many mobile browsers include "Mobile" + // Exclude obvious desktop platforms + ua = ua.ToLowerInvariant(); + + if (ua.Contains("ipad") || ua.Contains("iphone") || ua.Contains("ipod")) + return true; + + if (ua.Contains("android") && ua.Contains("mobile")) + return true; + + if (ua.Contains("android") && !ua.Contains("mobile")) + { + // Likely a tablet; treat as mobile for your use case + return true; + } + + if (ua.Contains("mobile")) + return true; + + // Desktop hints + if (ua.Contains("windows nt") || ua.Contains("macintosh") || ua.Contains("x11")) + return false; + + // Fallback + return false; + } + + // ========================= + // Existing helpers + // ========================= + + private string GenerateBookSchemaJsonLd(Reviews reviews, int take) + { + const string imageUrl = "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"; + const string baseUrl = "https://www.catherinelynwood.com/the-alpha-flame/discovery"; + + var schema = new Dictionary + { + ["@context"] = "https://schema.org", + ["@type"] = "Book", + ["name"] = "The Alpha Flame: Discovery", + ["alternateName"] = "The Alpha Flame Book 1", + ["image"] = imageUrl, + ["author"] = new Dictionary + { + ["@type"] = "Person", + ["name"] = "Catherine Lynwood", + ["url"] = "https://www.catherinelynwood.com" + }, + ["publisher"] = new Dictionary + { + ["@type"] = "Organization", + ["name"] = "Catherine Lynwood" + }, + ["datePublished"] = "2025-08-21", + ["description"] = "The Alpha Flame: Discovery is a powerful, character-driven novel set in 1983 Birmingham, following Maggie Grant and Beth, two young women separated by fate, reunited by truth, and bound by secrets...", + ["genre"] = "Women's Fiction, Mystery, Contemporary Historical", + ["inLanguage"] = "en-GB", + ["url"] = baseUrl + }; + + if (reviews?.Items?.Any() == true) + { + var reviewObjects = new List>(); + double total = 0; + foreach (var review in reviews.Items) total += review.RatingValue; + + foreach (var review in reviews.Items.Take(take)) + { + reviewObjects.Add(new Dictionary + { + ["@type"] = "Review", + ["author"] = new Dictionary + { + ["@type"] = "Person", + ["name"] = review.AuthorName + }, + ["datePublished"] = review.DatePublished.ToString("yyyy-MM-dd"), + ["reviewBody"] = StripHtml(review.ReviewBody), + ["reviewRating"] = new Dictionary + { + ["@type"] = "Rating", + ["ratingValue"] = review.RatingValue, + ["bestRating"] = "5" + } + }); + } + + schema["review"] = reviewObjects; + schema["aggregateRating"] = new Dictionary + { + ["@type"] = "AggregateRating", + ["ratingValue"] = (total / reviews.Items.Count).ToString("0.0", CultureInfo.InvariantCulture), + ["reviewCount"] = reviews.Items.Count + }; + } + + schema["workExample"] = new List> + { + new Dictionary + { + ["@type"] = "Book", + ["bookFormat"] = "https://schema.org/Hardcover", + ["isbn"] = "978-1-0682258-0-2", + ["name"] = "The Alpha Flame: Discovery – Collector's Edition", + ["image"] = imageUrl, + ["offers"] = new Dictionary + { + ["@type"] = "Offer", + ["price"] = "23.99", + ["priceCurrency"] = "GBP", + ["availability"] = "https://schema.org/InStock", + ["url"] = baseUrl + } + }, + new Dictionary + { + ["@type"] = "Book", + ["bookFormat"] = "https://schema.org/Paperback", + ["isbn"] = "978-1-0682258-1-9", + ["name"] = "The Alpha Flame: Discovery – Bookshop Edition", + ["image"] = imageUrl, + ["offers"] = new Dictionary + { + ["@type"] = "Offer", + ["price"] = "17.99", + ["priceCurrency"] = "GBP", + ["availability"] = "https://schema.org/InStock", + ["url"] = baseUrl + } + }, + new Dictionary + { + ["@type"] = "Book", + ["bookFormat"] = "https://schema.org/Paperback", + ["isbn"] = "978-1-0682258-2-6", + ["name"] = "The Alpha Flame: Discovery – Amazon Edition", + ["image"] = imageUrl, + ["offers"] = new Dictionary + { + ["@type"] = "Offer", + ["price"] = "13.99", + ["priceCurrency"] = "GBP", + ["availability"] = "https://schema.org/InStock", + ["url"] = baseUrl + } + }, + new Dictionary + { + ["@type"] = "Book", + ["bookFormat"] = "https://schema.org/EBook", + ["isbn"] = "978-1-0682258-3-3", + ["name"] = "The Alpha Flame: Discovery – eBook", + ["image"] = imageUrl, + ["offers"] = new Dictionary + { + ["@type"] = "Offer", + ["price"] = "3.95", + ["priceCurrency"] = "GBP", + ["availability"] = "https://schema.org/InStock", + ["url"] = baseUrl + } + } + }; + + return JsonConvert.SerializeObject(schema, Formatting.Indented); + } + + private static string StripHtml(string input) => + string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty); + } +} diff --git a/CatherineLynwood/Controllers/SitemapController.cs b/CatherineLynwood/Controllers/SitemapController.cs index 6c58186..f6b8737 100644 --- a/CatherineLynwood/Controllers/SitemapController.cs +++ b/CatherineLynwood/Controllers/SitemapController.cs @@ -38,6 +38,7 @@ 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("AudioBook", "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 }, @@ -46,6 +47,7 @@ namespace CatherineLynwood.Controllers 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", "Reckoning", 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 }, @@ -53,6 +55,7 @@ namespace CatherineLynwood.Controllers 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 }, + new SitemapEntry { Url = Url.Action("LarhysaSaddul", "Collaborations", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow }, // Additional static pages }; diff --git a/CatherineLynwood/Controllers/TheAlphaFlameController.cs b/CatherineLynwood/Controllers/TheAlphaFlameController.cs index 25451a8..263aad3 100644 --- a/CatherineLynwood/Controllers/TheAlphaFlameController.cs +++ b/CatherineLynwood/Controllers/TheAlphaFlameController.cs @@ -214,11 +214,6 @@ namespace CatherineLynwood.Controllers return View(); } - [Route("reckoning")] - public IActionResult Reckoning() - { - return View(); - } [Route("redemption")] public IActionResult Redemption() diff --git a/CatherineLynwood/Middleware/BlockPhpRequestsMiddleware.cs b/CatherineLynwood/Middleware/BlockPhpRequestsMiddleware.cs deleted file mode 100644 index 04d0e1b..0000000 --- a/CatherineLynwood/Middleware/BlockPhpRequestsMiddleware.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace CatherineLynwood.Middleware -{ - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Logging; - using Microsoft.Web.Administration; - - using System.Linq; - using System.Threading.Tasks; - - public class BlockPhpRequestsMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private IWebHostEnvironment _environment; - - public BlockPhpRequestsMiddleware(RequestDelegate next, ILogger logger, IWebHostEnvironment environment) - { - _next = next; - _logger = logger; - _environment = environment; - } - - public async Task InvokeAsync(HttpContext context) - { - var requestPath = context.Request.Path.Value; - - if (requestPath != null && (requestPath.EndsWith(".php") || requestPath.EndsWith(".env"))) - { - var ipAddress = context.Connection.RemoteIpAddress?.ToString(); - if (ipAddress != null) - { - _logger.LogWarning($"Detected PHP request from IP {ipAddress}."); - - if (!_environment.IsDevelopment()) - { - // Only attempt to block IP if not in development - BlockIpAddressInIIS(ipAddress); - } - - context.Response.StatusCode = StatusCodes.Status403Forbidden; - return; - } - } - - await _next(context); - } - - - private void BlockIpAddressInIIS(string ipAddress) - { - using (var serverManager = new ServerManager()) - { - // Replace "Default Web Site" with your actual site name - var site = serverManager.Sites["CatherineLynwood"]; - var config = site.GetWebConfiguration(); - var ipSecuritySection = config.GetSection("system.webServer/security/ipSecurity"); - - var ipSecurityCollection = ipSecuritySection.GetCollection(); - - // Check if IP already exists in the list to avoid duplicates - var existingEntry = ipSecurityCollection.FirstOrDefault(e => e.Attributes["ipAddress"]?.Value?.ToString() == ipAddress); - if (existingEntry == null) - { - // Add a new IP restriction entry with deny access - var addElement = ipSecurityCollection.CreateElement("add"); - addElement.SetAttributeValue("ipAddress", ipAddress); - addElement.SetAttributeValue("allowed", false); - ipSecurityCollection.Add(addElement); - - serverManager.CommitChanges(); - } - } - } - } - -} diff --git a/CatherineLynwood/Middleware/BotFilterMiddleware.cs b/CatherineLynwood/Middleware/BotFilterMiddleware.cs deleted file mode 100644 index f62430d..0000000 --- a/CatherineLynwood/Middleware/BotFilterMiddleware.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace CatherineLynwood.Middleware -{ - public class BotFilterMiddleware - { - #region Private Fields - - private static readonly List BadBots = new() - { - "AhrefsBot", "SemrushBot", "MJ12bot", "DotBot", "Baiduspider", "YandexBot" - }; - - private readonly RequestDelegate _next; - - #endregion Private Fields - - #region Public Constructors - - public BotFilterMiddleware(RequestDelegate next) => _next = next; - - #endregion Public Constructors - - #region Public Methods - - public async Task Invoke(HttpContext context) - { - var userAgent = context.Request.Headers["User-Agent"].ToString(); - if (BadBots.Any(bot => userAgent.Contains(bot, StringComparison.OrdinalIgnoreCase))) - { - context.Response.StatusCode = 403; - await context.Response.WriteAsync("Forbidden"); - return; - } - - await _next(context); - } - - #endregion Public Methods - } -} \ No newline at end of file diff --git a/CatherineLynwood/Middleware/GeoResolutionMiddleware.cs b/CatherineLynwood/Middleware/GeoResolutionMiddleware.cs index dac225c..7d47ebd 100644 --- a/CatherineLynwood/Middleware/GeoResolutionMiddleware.cs +++ b/CatherineLynwood/Middleware/GeoResolutionMiddleware.cs @@ -24,20 +24,52 @@ namespace CatherineLynwood.Middleware public async Task Invoke(HttpContext context) { + var path = context.Request.Path.ToString(); + + // Skip static files (images, css, js, fonts, ico, svg, etc.) + if (path.EndsWith(".css", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".ico", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".woff", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".woff2", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".map", StringComparison.OrdinalIgnoreCase)) + { + await _next(context); + return; + } + + // Optionally also skip non-GET requests if you only care about page views + if (!HttpMethods.IsGet(context.Request.Method)) + { + await _next(context); + return; + } // Basic trace to confirm middleware is running _logger.LogInformation("GeoMW: path={Path}", context.Request.Path); var ip = GetClientIp(context); var userAgent = context.Request.Headers["User-Agent"].ToString(); + var queryString = context.Request.QueryString.ToString(); // In Development, replace loopback with a known public IP so ResolveAsync definitely runs if (ip is null || IPAddress.IsLoopback(ip)) { if (_env.IsDevelopment()) { - ip = IPAddress.Parse("81.145.211.224"); // UK + ip = IPAddress.Parse("90.240.25.56"); // UK //ip = IPAddress.Parse("66.249.10.10"); // US //ip = IPAddress.Parse("24.48.0.1"); // Canada + //ip = IPAddress.Parse("212.129.83.32"); // Ireland + //ip = IPAddress.Parse("203.109.171.243"); // New Zealand //ip = IPAddress.Parse("1.1.1.1"); // Australia _logger.LogInformation("GeoMW: dev override IP -> {IP}", ip); } @@ -51,7 +83,7 @@ namespace CatherineLynwood.Middleware _logger.LogInformation("GeoMW: calling resolver for {IP}", ip); - var geo = await _resolver.ResolveAsync(ip, userAgent); + var geo = await _resolver.ResolveAsync(ip, path, queryString, userAgent); if (geo is not null) { diff --git a/CatherineLynwood/Middleware/IpqsBlockMiddleware.cs b/CatherineLynwood/Middleware/IpqsBlockMiddleware.cs deleted file mode 100644 index 936362d..0000000 --- a/CatherineLynwood/Middleware/IpqsBlockMiddleware.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Net; - -namespace CatherineLynwood.Middleware -{ - public class IpqsBlockMiddleware - { - private readonly RequestDelegate _next; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - private const string ApiKey = "MQUwnYmhKZzHpt6FyxV97EFg8JxlByZt"; // Replace with your IPQS API key - - private static readonly string[] ProtectedPaths = new[] - { - "/ask-a-question", - "/contact-catherine", - "/the-alpha-flame/blog/" - }; - - public IpqsBlockMiddleware(RequestDelegate next, IHttpClientFactory httpClientFactory, ILogger logger) - { - _next = next; - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - public async Task Invoke(HttpContext context) - { - var path = context.Request.Path.Value?.ToLower(); - if (!ProtectedPaths.Any(p => path.StartsWith(p))) - { - await _next(context); - return; - } - - var ip = context.Connection.RemoteIpAddress?.ToString(); - - //ip = "18.130.131.76"; - - // skip localhost - if (string.IsNullOrWhiteSpace(ip) || IPAddress.IsLoopback(IPAddress.Parse(ip))) - { - await _next(context); - return; - } - - try - { - var client = _httpClientFactory.CreateClient(); - var url = $"https://ipqualityscore.com/api/json/ip/{ApiKey}/{ip}?strictness=1&fast=true"; - var response = await client.GetFromJsonAsync(url); - - if (response != null && (response.is_proxy || response.is_vpn || response.fraud_score >= 85)) - { - if (context.Request.Method == HttpMethods.Post) - { - _logger.LogWarning("Blocked VPN/Proxy on POST: IP={IP}", ip); - context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsync("Access denied."); - return; - } - - // Mark for GET requests so the view can show a warning - context.Items["IsVpn"] = true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "IPQS lookup failed"); - } - - await _next(context); - } - - public class IpqsResponse - { - public bool is_proxy { get; set; } - public bool is_vpn { get; set; } - public int fraud_score { get; set; } - } - } - -} diff --git a/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs b/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs deleted file mode 100644 index f04225f..0000000 --- a/CatherineLynwood/Middleware/RedirectToWwwMiddleware.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace CatherineLynwood.Middleware -{ - public class RedirectToWwwMiddleware - { - private readonly RequestDelegate _next; - private IWebHostEnvironment _environment; - - public RedirectToWwwMiddleware(RequestDelegate next, IWebHostEnvironment environment) - { - _next = next; - _environment = environment; - } - - public async Task InvokeAsync(HttpContext context) - { - var host = context.Request.Host.Host; - var schema = context.Request.Scheme; - - if (_environment.IsProduction()) - { - if (host.Equals("catherinelynwood.com", StringComparison.OrdinalIgnoreCase) || schema.Equals("http", StringComparison.OrdinalIgnoreCase)) - { - var newUrl = $"https://www.catherinelynwood.com{context.Request.Path}{context.Request.QueryString}"; - context.Response.StatusCode = StatusCodes.Status308PermanentRedirect; - context.Response.Headers["Location"] = newUrl; - return; - } - } - - - - // Continue to the next middleware. - await _next(context); - } - } -} diff --git a/CatherineLynwood/Middleware/RefererValidationMiddleware.cs b/CatherineLynwood/Middleware/RefererValidationMiddleware.cs deleted file mode 100644 index 0540cdf..0000000 --- a/CatherineLynwood/Middleware/RefererValidationMiddleware.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace CatherineLynwood.Middleware -{ - public class RefererValidationMiddleware - { - private readonly RequestDelegate _next; - - // Whitelist of valid referer prefixes - private static readonly string[] AllowedReferers = new[] - { - "https://www.catherinelynwood.com", - "http://localhost", - "https://localhost" - }; - - public RefererValidationMiddleware(RequestDelegate next) - { - _next = next; - } - - public async Task Invoke(HttpContext context) - { - if (context.Request.Method == HttpMethods.Post) - { - var referer = context.Request.Headers["Referer"].ToString(); - - if (string.IsNullOrEmpty(referer) || !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.OrdinalIgnoreCase))) - { - context.Response.StatusCode = StatusCodes.Status451UnavailableForLegalReasons; - await context.Response.WriteAsync("Invalid request."); - return; - } - } - - await _next(context); - } - } - -} diff --git a/CatherineLynwood/Models/BuyCatalog.cs b/CatherineLynwood/Models/BuyCatalog.cs deleted file mode 100644 index 1920e42..0000000 --- a/CatherineLynwood/Models/BuyCatalog.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System.Collections.ObjectModel; - -namespace CatherineLynwood.Models -{ - public static class BuyCatalog - { - private static readonly Dictionary Links = new() - { - // --- Ingram direct (GB/US only) --- - ["ingram-hardback-gb"] = new BuyLink - { - Slug = "ingram-hardback-gb", - Url = "https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO", - Retailer = "Ingram", - Format = "Hardback", - CountryGroup = "GB" - }, - ["ingram-paperback-gb"] = new BuyLink - { - Slug = "ingram-paperback-gb", - Url = "https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ", - Retailer = "Ingram", - Format = "Paperback", - CountryGroup = "GB" - }, - ["ingram-hardback-us"] = new BuyLink - { - Slug = "ingram-hardback-us", - Url = "https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO", - Retailer = "Ingram", - Format = "Hardback", - CountryGroup = "US" - }, - ["ingram-paperback-us"] = new BuyLink - { - Slug = "ingram-paperback-us", - Url = "https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ", - Retailer = "Ingram", - Format = "Paperback", - CountryGroup = "US" - }, - - // --- Amazon (GB/US/CA/AU) --- - ["amazon-hardback-gb"] = new BuyLink - { - Slug = "amazon-hardback-gb", - Url = "https://www.amazon.co.uk/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "GB" - }, - ["amazon-paperback-gb"] = new BuyLink - { - Slug = "amazon-paperback-gb", - Url = "https://www.amazon.co.uk/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "GB" - }, - ["amazon-kindle-gb"] = new BuyLink - { - Slug = "amazon-kindle-gb", - Url = "https://www.amazon.co.uk/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "GB" - }, - - ["amazon-hardback-us"] = new BuyLink - { - Slug = "amazon-hardback-us", - Url = "https://www.amazon.com/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "US" - }, - ["amazon-paperback-us"] = new BuyLink - { - Slug = "amazon-paperback-us", - Url = "https://www.amazon.com/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "US" - }, - ["amazon-kindle-us"] = new BuyLink - { - Slug = "amazon-kindle-us", - Url = "https://www.amazon.com/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "US" - }, - - ["amazon-hardback-ca"] = new BuyLink - { - Slug = "amazon-hardback-ca", - Url = "https://www.amazon.ca/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "CA" - }, - ["amazon-paperback-ca"] = new BuyLink - { - Slug = "amazon-paperback-ca", - Url = "https://www.amazon.ca/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "CA" - }, - ["amazon-kindle-ca"] = new BuyLink - { - Slug = "amazon-kindle-ca", - Url = "https://www.amazon.ca/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "CA" - }, - - ["amazon-hardback-au"] = new BuyLink - { - Slug = "amazon-hardback-au", - Url = "https://www.amazon.com.au/dp/1068225807", - Retailer = "Amazon", - Format = "Hardback", - CountryGroup = "AU" - }, - ["amazon-paperback-au"] = new BuyLink - { - Slug = "amazon-paperback-au", - Url = "https://www.amazon.com.au/dp/1068225815", - Retailer = "Amazon", - Format = "Paperback", - CountryGroup = "AU" - }, - ["amazon-kindle-au"] = new BuyLink - { - Slug = "amazon-kindle-au", - Url = "https://www.amazon.com.au/dp/B0FBS427VD", - Retailer = "Amazon", - Format = "Kindle", - CountryGroup = "AU" - }, - - // --- National retailers --- - ["waterstones-hardback-gb"] = new BuyLink - { - Slug = "waterstones-hardback-gb", - Url = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802", - Retailer = "Waterstones", - Format = "Hardback", - CountryGroup = "GB" - }, - ["waterstones-paperback-gb"] = new BuyLink - { - Slug = "waterstones-paperback-gb", - Url = "https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819", - Retailer = "Waterstones", - Format = "Paperback", - CountryGroup = "GB" - }, - - ["bn-hardback-us"] = new BuyLink - { - Slug = "bn-hardback-us", - Url = "https://www.barnesandnoble.com/s/9781068225802", - Retailer = "Barnes & Noble", - Format = "Hardback", - CountryGroup = "US" - }, - ["bn-paperback-us"] = new BuyLink - { - Slug = "bn-paperback-us", - Url = "https://www.barnesandnoble.com/s/9781068225819", - Retailer = "Barnes & Noble", - Format = "Paperback", - CountryGroup = "US" - }, - - // --- Apple Books (GB/US/CA/AU/IE) --- - ["apple-ebook-gb"] = new BuyLink - { - Slug = "apple-ebook-gb", - Url = "https://books.apple.com/gb/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "GB" - }, - ["apple-ebook-us"] = new BuyLink - { - Slug = "apple-ebook-us", - Url = "https://books.apple.com/us/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "US" - }, - ["apple-ebook-ca"] = new BuyLink - { - Slug = "apple-ebook-ca", - Url = "https://books.apple.com/ca/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "CA" - }, - ["apple-ebook-au"] = new BuyLink - { - Slug = "apple-ebook-au", - Url = "https://books.apple.com/au/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "AU" - }, - ["apple-ebook-ie"] = new BuyLink - { - Slug = "apple-ebook-ie", - Url = "https://books.apple.com/ie/book/the-alpha-flame/id/6747852729", - Retailer = "Apple Books", - Format = "eBook", - CountryGroup = "IE" - }, - - // --- Kobo (GB/US/CA/AU/IE) --- - ["kobo-ebook-gb"] = new BuyLink - { - Slug = "kobo-ebook-gb", - Url = "https://www.kobo.com/gb/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "GB" - }, - ["kobo-ebook-us"] = new BuyLink - { - Slug = "kobo-ebook-us", - Url = "https://www.kobo.com/us/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "US" - }, - ["kobo-ebook-ca"] = new BuyLink - { - Slug = "kobo-ebook-ca", - Url = "https://www.kobo.com/ca/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "CA" - }, - ["kobo-ebook-au"] = new BuyLink - { - Slug = "kobo-ebook-au", - Url = "https://www.kobo.com/au/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "AU" - }, - ["kobo-ebook-ie"] = new BuyLink - { - Slug = "kobo-ebook-ie", - Url = "https://www.kobo.com/ie/en/ebook/the-alpha-flame", - Retailer = "Kobo", - Format = "eBook", - CountryGroup = "IE" - } - }; - - public static BuyLink? Find(string slug) => - Links.TryGetValue(slug, out var link) ? link : null; - - public static string Url(string slug) => - Links.TryGetValue(slug, out var link) ? link.Url : ""; - } -} diff --git a/CatherineLynwood/Models/BuyLink.cs b/CatherineLynwood/Models/BuyLink.cs deleted file mode 100644 index f90031d..0000000 --- a/CatherineLynwood/Models/BuyLink.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace CatherineLynwood.Models -{ - public class BuyLink - { - #region Public Properties - - public string CountryGroup { get; set; } - - public string Format { get; set; } - - public string Retailer { get; set; } - - public string Slug { get; set; } - - public string Url { get; set; } - - #endregion Public Properties - } -} \ No newline at end of file diff --git a/CatherineLynwood/Models/BuyLinksViewModel.cs b/CatherineLynwood/Models/BuyLinksViewModel.cs deleted file mode 100644 index 429640c..0000000 --- a/CatherineLynwood/Models/BuyLinksViewModel.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace CatherineLynwood.Models -{ - public sealed class BuyLinksViewModel - { - #region Public Properties - - public LinkChoice AmazonHardback { get; set; } = new(); - - public LinkChoice AmazonKindle { get; set; } = new(); - - public LinkChoice AmazonPaperback { get; set; } = new(); - - public LinkChoice Apple { get; set; } = new(); - - public string Country { get; set; } = "GB"; - - public LinkChoice? IngramHardback { get; set; } - - public string? IngramHardbackPrice { get; set; } - - public LinkChoice? IngramPaperback { get; set; } - - public string? IngramPaperbackPrice { get; set; } - - public LinkChoice Kobo { get; set; } = new(); - - public LinkChoice? NationalHardback { get; set; } - - public string? NationalLabel { get; set; } - - public LinkChoice? NationalPaperback { get; set; } - - #endregion Public Properties - } -} \ No newline at end of file diff --git a/CatherineLynwood/Models/BuyPanelViewModel.cs b/CatherineLynwood/Models/BuyPanelViewModel.cs new file mode 100644 index 0000000..3380186 --- /dev/null +++ b/CatherineLynwood/Models/BuyPanelViewModel.cs @@ -0,0 +1,71 @@ +namespace CatherineLynwood.Models +{ + public class BuyGroup + { + #region Public Properties + + public int BuyGroupID { get; set; } + + public int DisplayOrder { get; set; } + + public string GroupName { get; set; } = string.Empty; + + public List Links { get; set; } = new List(); + + public string Message { get; set; } = string.Empty; + + #endregion Public Properties + } + + public class BuyLink + { + #region Public Properties + + public int BuyGroupID { get; set; } + + public int BuyLinkID { get; set; } + + public string ISO2 { get; set; } = string.Empty; + + public string Icon { get; set; } = string.Empty; + + public string Price { get; set; } = string.Empty; + + public string Slug { get; set; } = string.Empty; + + public string Target { get; set; } = string.Empty; + + public string Text { get; set; } = string.Empty; + + #endregion Public Properties + } + + public class BuyPanelViewModel + { + #region Public Properties + + private string _iso2 = "UK"; + + public string CountryName { get; set; } = string.Empty; + + public string FlagUrl + { + get + { + return $"/images/flags/{_iso2.ToLower()}.svg"; + } + } + + public List Groups { get; set; } = new List(); + + public string ISO2 + { + get { return _iso2; } + set { _iso2 = value; } + } + + public string Src { get; set; } = string.Empty; + + #endregion Public Properties + } +} \ No newline at end of file diff --git a/CatherineLynwood/Models/GeoIpResult.cs b/CatherineLynwood/Models/GeoIpResult.cs index fa110e6..2e03aa1 100644 --- a/CatherineLynwood/Models/GeoIpResult.cs +++ b/CatherineLynwood/Models/GeoIpResult.cs @@ -4,6 +4,8 @@ { #region Public Properties + public string City { get; set; } = string.Empty; + public string CountryName { get; set; } = ""; public string Ip { get; set; } = ""; @@ -12,9 +14,13 @@ public DateTime LastSeenUtc { get; set; } - public string Source { get; set; } + public string Path { get; set; } = string.Empty; - public string UserAgent { get; set; } + public string QueryString { get; set; } = string.Empty; + + public string Source { get; set; } = string.Empty; + + public string UserAgent { get; set; } = string.Empty; #endregion Public Properties } diff --git a/CatherineLynwood/Models/DIscoveryPageViewModel.cs b/CatherineLynwood/Models/TitlePageViewModel.cs similarity index 50% rename from CatherineLynwood/Models/DIscoveryPageViewModel.cs rename to CatherineLynwood/Models/TitlePageViewModel.cs index 99821f7..d8299d7 100644 --- a/CatherineLynwood/Models/DIscoveryPageViewModel.cs +++ b/CatherineLynwood/Models/TitlePageViewModel.cs @@ -1,17 +1,16 @@ namespace CatherineLynwood.Models { - public sealed class DiscoveryPageViewModel + public sealed class TitlePageViewModel { #region Public Properties - // All slugs are for /go/{slug} and /track/click?slug={slug} - public BuyLinksViewModel Buy { get; set; } = new BuyLinksViewModel(); - public Reviews Reviews { get; set; } = new Reviews(); public string UserIso2 { get; set; } = "GB"; - public string Src = "Discovery"; + public string Src { get; set; } = string.Empty; + + public string Title { get; set; } = string.Empty; #endregion Public Properties } diff --git a/CatherineLynwood/Program.cs b/CatherineLynwood/Program.cs index 780a3ab..7ab859d 100644 --- a/CatherineLynwood/Program.cs +++ b/CatherineLynwood/Program.cs @@ -121,8 +121,6 @@ namespace CatherineLynwood app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); - - // Resolve ISO2 once per request, stored in HttpContext.Items via GeoResolutionMiddleware app.UseMiddleware(); app.UseHttpsRedirection(); diff --git a/CatherineLynwood/Services/DataAccess.cs b/CatherineLynwood/Services/DataAccess.cs index b309bcd..2a80203 100644 --- a/CatherineLynwood/Services/DataAccess.cs +++ b/CatherineLynwood/Services/DataAccess.cs @@ -27,6 +27,76 @@ namespace CatherineLynwood.Services _connectionString = connectionString; } + public async Task GetBuyPanelViewModel(string iso2, string title) + { + BuyPanelViewModel buyPanelViewModel = new BuyPanelViewModel(); + + using (SqlConnection conn = new SqlConnection(_connectionString)) + { + using (SqlCommand cmd = new SqlCommand()) + { + try + { + await conn.OpenAsync(); + cmd.Connection = conn; + cmd.CommandType = CommandType.StoredProcedure; + cmd.CommandText = "GetBuyLinks"; + cmd.Parameters.AddWithValue("@ISO2", iso2); + cmd.Parameters.AddWithValue("@Title", title); + + using (SqlDataReader rdr = await cmd.ExecuteReaderAsync()) + { + while (await rdr.ReadAsync()) + { + buyPanelViewModel.ISO2 = GetDataString(rdr, "ISO2"); + buyPanelViewModel.CountryName = GetDataString(rdr, "CountryName"); + } + + await rdr.NextResultAsync(); + + while (await rdr.ReadAsync()) + { + BuyGroup buyGroup = new BuyGroup + { + BuyGroupID = GetDataInt(rdr, "BuyGroupID"), + Message = GetDataString(rdr, "Message"), + DisplayOrder = GetDataInt(rdr, "DisplayOrder"), + GroupName = GetDataString(rdr, "GroupName") + }; + buyPanelViewModel.Groups.Add(buyGroup); + } + + await rdr.NextResultAsync(); + + while (await rdr.ReadAsync()) + { + BuyLink buyLink = new BuyLink + { + BuyGroupID = GetDataInt(rdr, "BuyGroupID"), + BuyLinkID = GetDataInt(rdr, "BuyLinkID"), + ISO2 = GetDataString(rdr, "ISO2"), + Price = GetDataString(rdr, "Price"), + Slug = GetDataString(rdr, "Slug"), + Target = GetDataString(rdr, "Target"), + Icon = GetDataString(rdr, "Icon"), + Text = GetDataString(rdr, "Text") + }; + var group = buyPanelViewModel.Groups.Find(g => g.BuyGroupID == buyLink.BuyGroupID); + if (group != null) + { + group.Links.Add(buyLink); + } + } + } + } + catch (Exception ex) + { + } + } + } + + return buyPanelViewModel; + } public async Task AddBlogCommentAsync(BlogComment blogComment) { bool visible = false; @@ -611,7 +681,9 @@ namespace CatherineLynwood.Services CountryName = GetDataString(rdr, "CountryName"), LastSeenUtc = GetDataDate(rdr, "LastSeenUtc"), Source = GetDataString(rdr, "Source"), - UserAgent = GetDataString(rdr, "UserAgent") + UserAgent = GetDataString(rdr, "UserAgent"), + Path = GetDataString(rdr, "Path"), + City = GetDataString(rdr, "City") }; } } @@ -702,7 +774,7 @@ namespace CatherineLynwood.Services return selectListItems; } - public async Task GetReviewsAsync() + public async Task GetReviewsAsync(string title) { Reviews reviews = new Reviews(); @@ -716,6 +788,7 @@ namespace CatherineLynwood.Services cmd.Connection = conn; cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = "GetReviews"; + cmd.Parameters.AddWithValue("@Title", title); using (SqlDataReader rdr = await cmd.ExecuteReaderAsync()) { @@ -928,19 +1001,15 @@ namespace CatherineLynwood.Services } public async Task SaveBuyClick( - DateTime dateTimeUtc, + DateTime dateTimeUtc, string slug, - string retailer, - string format, string countryGroup, string ip, string country, string userAgent, string referer, - string page, // the page on your site where the click happened string sessionId, - string queryString, // original request query (e.g., utms, country override) - string destinationUrl // the final retailer URL you redirect to + string src ) { bool success = true; @@ -958,8 +1027,6 @@ namespace CatherineLynwood.Services // Required cmd.Parameters.AddWithValue("@DateTimeUtc", dateTimeUtc); cmd.Parameters.AddWithValue("@Slug", (object?)slug ?? DBNull.Value); - cmd.Parameters.AddWithValue("@Retailer", (object?)retailer ?? DBNull.Value); - cmd.Parameters.AddWithValue("@Format", (object?)format ?? DBNull.Value); cmd.Parameters.AddWithValue("@CountryGroup", (object?)countryGroup ?? DBNull.Value); // Signals / attribution @@ -967,10 +1034,8 @@ namespace CatherineLynwood.Services cmd.Parameters.AddWithValue("@Country", (object?)country ?? DBNull.Value); cmd.Parameters.AddWithValue("@UserAgent", (object?)userAgent ?? DBNull.Value); cmd.Parameters.AddWithValue("@Referer", (object?)referer ?? DBNull.Value); - cmd.Parameters.AddWithValue("@Page", (object?)page ?? DBNull.Value); cmd.Parameters.AddWithValue("@SessionId", (object?)sessionId ?? DBNull.Value); - cmd.Parameters.AddWithValue("@QueryString", (object?)queryString ?? DBNull.Value); - cmd.Parameters.AddWithValue("@DestinationUrl", (object?)destinationUrl ?? DBNull.Value); + cmd.Parameters.AddWithValue("@Src", (object?)src ?? DBNull.Value); await cmd.ExecuteNonQueryAsync(); } @@ -1067,6 +1132,9 @@ namespace CatherineLynwood.Services cmd.Parameters.AddWithValue("@LastSeenUtc", geo.LastSeenUtc); cmd.Parameters.AddWithValue("@Source", geo.Source ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@UserAgent", geo.UserAgent ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@Path", geo.Path ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@QueryString", geo.QueryString ?? (object)DBNull.Value); + cmd.Parameters.AddWithValue("@City", geo.City ?? (object)DBNull.Value); await cmd.ExecuteNonQueryAsync(); } diff --git a/CatherineLynwood/Services/GeoResolver.cs b/CatherineLynwood/Services/GeoResolver.cs index 47f7793..e4c2742 100644 --- a/CatherineLynwood/Services/GeoResolver.cs +++ b/CatherineLynwood/Services/GeoResolver.cs @@ -23,7 +23,7 @@ namespace CatherineLynwood.Services _logger = logger; } - public async Task ResolveAsync(IPAddress ip, string userAgent, CancellationToken ct = default) + public async Task ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default) { var ipStr = ip.ToString(); @@ -42,13 +42,14 @@ namespace CatherineLynwood.Services try { var client = _httpClientFactory.CreateClient(nameof(GeoResolver)); - var json = await client.GetStringAsync($"http://ip-api.com/json/{ipStr}?fields=status,country,countryCode", ct); + var json = await client.GetStringAsync($"http://ip-api.com/json/{ipStr}", ct); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; if (root.GetProperty("status").GetString() == "success") { - var iso2 = root.GetProperty("countryCode").GetString() ?? "UN"; + var iso2 = root.GetProperty("countryCode").GetString() ?? "UK"; var country = root.GetProperty("country").GetString(); + var city = root.GetProperty("city").GetString() ?? ""; var geo = new GeoIpResult { @@ -57,7 +58,10 @@ namespace CatherineLynwood.Services CountryName = country ?? "", LastSeenUtc = DateTime.UtcNow, Source = "ip-api", - UserAgent = userAgent + UserAgent = userAgent, + Path = path, + QueryString = queryString, + City = city }; await _dataAccess.SaveGeoIpAsync(geo); diff --git a/CatherineLynwood/Services/IGeoResolver.cs b/CatherineLynwood/Services/IGeoResolver.cs index 0faacf0..b5d7d85 100644 --- a/CatherineLynwood/Services/IGeoResolver.cs +++ b/CatherineLynwood/Services/IGeoResolver.cs @@ -6,6 +6,6 @@ namespace CatherineLynwood.Services { public interface IGeoResolver { - Task ResolveAsync(IPAddress ip, string userAgent, CancellationToken ct = default); + Task ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default); } } diff --git a/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs b/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs index f88b2b7..ea53cce 100644 --- a/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs +++ b/CatherineLynwood/TagHelpers/ResponsiveImageTagHelper.cs @@ -2,9 +2,11 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using SixLabors.Fonts; +using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Processing; namespace CatherineLynwood.TagHelpers { diff --git a/CatherineLynwood/Views/Collaborations/LarhysaSaddul.cshtml b/CatherineLynwood/Views/Collaborations/LarhysaSaddul.cshtml new file mode 100644 index 0000000..687ece6 --- /dev/null +++ b/CatherineLynwood/Views/Collaborations/LarhysaSaddul.cshtml @@ -0,0 +1,134 @@ +@{ + ViewData["Title"] = "Larhysa Saddul – Voice of The Alpha Flame: Discovery"; +} + +
+
+
+ +
+
+ +
+
+

+ Larhysa Saddul +

+

+ Actress, singer, and voice artist – the voice of + The Alpha Flame: Discovery. +

+

+ With a background on stage, on screen, and in the studio, Larhysa brings + depth, warmth, and emotional honesty to every performance, from sharp-edged + comedy to powerful, character-driven drama. +

+ + About The Alpha Flame: Discovery + +@* + Audiobook information + *@ + + +

About Larhysa

+

+ Larhysa Saddul is an accomplished actress, singer, and voice artist whose career + spans more than a decade across stage, screen, and studio. With roots in theatre + and musical performance, she has built a varied body of work that showcases her + vocal range, comic timing, and emotional depth. +

+

+ On stage, she is known for standout roles such as the Evil Stepmother in + Cinder’Aliyah, where she performed alongside BBC’s Abdullah Afzal, and for + her work in the acclaimed, five-star Edinburgh Fringe production + NewsRevue, contributing as both performer and writer. Her blend of sharp + character work and musical storytelling has made her a natural fit for + contemporary, fast-paced theatre. +

+

+ Building on her acting experience and instinct for story, Larhysa has expanded her + creative work into voice acting, bringing characters and narratives to life with + authenticity, warmth, and emotional nuance. Her narration of Catherine Lynwood’s + psychological crime novel The Alpha Flame: Discovery marks an exciting + new chapter in her career. +

+

+ Through this performance, Larhysa seeks to honour the story’s powerful themes of + justice, sisterhood, and survival, giving listeners an intimate way into Maggie + and Beth’s world and the complex emotional landscape they inhabit. +

+
+
+ +

+ Images courtesy of Larhysa Saddul, used with permission. +

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

Hear Larhysa in action

+

+ Here are a few excerpts from Larhysa's excellent narration of The Alphe Flame: Discovery. +

+ +

+ Excerpt taken from Chapter 25 +

+ +

+ Excerpt taken from Chapter 32 +

+
+
+
+ +
+
+
+

Interview: Catherine & Larhysa

+

+ In this in-depth conversation, author Catherine Lynwood and Larhysa Saddul talk + about adapting The Alpha Flame: Discovery for audio, the emotional weight + of the story, and the craft that goes into bringing Maggie and Beth to life in + the recording booth. +

+

+ Listen to the full interview +

+ +

+ Duration: ~25 minutes. +

+ + +
+
+
+
+
diff --git a/CatherineLynwood/Views/Discovery/AudioBook.cshtml b/CatherineLynwood/Views/Discovery/AudioBook.cshtml new file mode 100644 index 0000000..7d1192d --- /dev/null +++ b/CatherineLynwood/Views/Discovery/AudioBook.cshtml @@ -0,0 +1,273 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Audiobook"; +} + +
+ +
+
+ +
+
+ + +
+
+

Listen to The Alpha Flame: Discovery

+

+ A slow-burn, character-driven story that hits differently through headphones. +

+ +
+ 🎧 17.5 hours + 🎙️ Fully narrated + 🔒 Exclusive to Audible +
+ + + +

+ Tip: Try one minute. If the voice doesn’t pull you in, close the page and go back to doomscrolling. +

+
+ +
+ +
+
+
+ +
+
Narration
+
Narrated by Lahrysa Saddul
+ +
    +
  • • Best for: drives, late nights, walks
  • +
  • • Style: intimate, story-first, no gimmicks
  • +
  • • No spoilers in the clips below
  • +
+
+
+
+
+ +
+
+ Already read it? + The audiobook isn’t a repeat, it’s a different experience. Scenes land differently when you can’t rush them. +
+
+
+
+ + +
+
+
+
+
🎧 Best with headphones
+
+ This is the kind of narration where quiet details matter. +
+
+
+
+
+
+
+
🌙 Best when it’s late
+
+ When your brain is tired and your eyes refuse to read another page. +
+
+
+
+
+
+
+
🚗 Best on long drives
+
+ Perfect for motorways, rain, and the kind of silence that gets loud. +
+
+
+
+
+ + +
+
+

Audio clips

+ Short, spoiler-safe moments to sample the voice and tone. +
+ +
+ +
+
+
+
Moment 1
+

“She saved me.”

+

+ Shock hasn’t worn off yet. Voices are low. One truth slips out that changes how everyone in the room sees her. +

+ + + +
+ ⏱ 0:41 + Emotion-led, post-shock +
+
+
+
+ + +
+
+
+
Moment 2
+

Voices through a door

+

+ A party outside. A quiet room inside. And words that were never meant to be overheard. +

+ + + +
+ ⏱ 1:06 + Rising unease +
+
+
+
+ + +
+
+
+
Moment 3
+

Betrayal doesn’t shout

+

+ Accusations come out sideways. Trust fractures in a place no one ever meant to stop. +

+ + + +
+ ⏱ 0:54 + Raw, confrontational +
+
+
+
+ +
+
+ + + +
+
+
+
+

What this audiobook is

+
    +
  • A story you live inside, not one you sprint through.
  • +
  • Character-led tension, emotional weight, and atmosphere that builds.
  • +
  • Narration that gives scenes room to breathe, pauses included.
  • +
  • Ideal if you like long, immersive listens rather than quick hits.
  • +
+
+
+
+ +
+
+
+

Quick details

+
+
Length
+
17.5 hours
+ +
Format
+
Audible audiobook
+ +
Availability
+
Exclusive to Audible
+ +
Best for
+
Headphones, drives, late nights
+
+
+
+
+
+ + +
+
+
+
Ready to listen?
+
+ Click through to Audible. If you’re a member, a credit is usually less painful than buying hardbacks at full price. +
+
+ +
+
+ +
+ +@section Scripts{ + + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Discovery/Chapter1.cshtml b/CatherineLynwood/Views/Discovery/Chapter1.cshtml index 1c77d99..84ec506 100644 --- a/CatherineLynwood/Views/Discovery/Chapter1.cshtml +++ b/CatherineLynwood/Views/Discovery/Chapter1.cshtml @@ -1,77 +1,78 @@ @{ ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1"; } - -
-
- -
-
- - -
-

Chapter 1 - Drowning in Silence - Beth

-

An exclusive glimpse into Beth's story

-
- - -
- -
- +
+
+
+ +
- -
-
+ +
+

Chapter 1 - Drowning in Silence - Beth

+

An exclusive glimpse into Beth's story

+
-
- + +
+ +
+ +
-
-

- Watch Beth narrating part of Chapter 1 - Drowning in Silence. -

- -
- -

- Listen to Beth narrating the complete Chapter 1 - Drowning in Silence. -

-
- -
-

Drowning in Silence - Beth

-

- I’d never known silence like that before. The kind that creeps under your skin and settles in your bones, sinking in so deep it feels like it might smother you. When I opened the door, that silence wrapped itself around me, choking me, filling me up until there was nothing else. I didn’t even know what I was seeing at first. I think maybe my mind tried to protect me, tried to shield me from what was right in front of me, even though I knew, deep down, that everything was about to change. -

-

- She was slumped there in the bath, water cold and still around her, her face as blank as a wax doll’s, skin washed out, lifeless. The first thought I had, the thing I’ll never forgive myself for, was how wrong it looked. It felt surreal, like a trick. This wasn’t her. It couldn’t be. My mum wasn’t a drinker, not like this, not ever, but there was an empty bottle lying on its side beside the bath, rolling slightly as I opened the door wider. It felt like it was mocking me, daring me to believe what I was seeing. -

-

- I felt sick, my throat clenching, my stomach twisting, and for a moment, I hated her, or whoever had done this to her. Hated the absurdity, the impossibility of it. She’d never have chosen that bottle over me, over herself. And yet there it was, an empty accusation, staring at me from the floor, her face pale and her lips blue. I couldn’t make sense of it. I just stood there, a dead thing staring back at her, just as lifeless as she was. -

-

- They say your life flashes before your eyes when you die, but I think they’re wrong. I think it’s the people left behind, the ones who have to see it, who have to stand there, watching their entire world collapse around them. I saw everything; all the tiny pieces of a life she’d held together for me, every smile, every reassuring word, every single thing that had kept me safe. And I realised, right then, that I was all alone. Utterly and completely alone. -

-

- There’s something that breaks in you when you lose everything in one heartbeat. It’s like the walls inside you just give way, crumbling into nothing, until all that’s left is this empty shell. I felt it, that shattering, like glass splintering into a million pieces inside my chest. I remember gripping the doorframe so hard my knuckles turned white, the pain grounding me, keeping me from slipping into whatever dark pit was opening up beneath my feet. I couldn’t look away from her. I couldn’t move, couldn’t breathe. I was frozen, trapped in this nightmare that wouldn’t end, a part of me hoping that if I stared long enough, I’d wake up. That this would all just go away. -

-

- But it didn’t. And I knew it wouldn’t. Because that was the moment my life ended too. She may have been the one in the water, but I was drowning right along with her... + +

+
+ +
+ + +
+

+ Watch Beth narrating part of Chapter 1 - Drowning in Silence.

+ +
+ +

+ Listen to Beth narrating the complete Chapter 1 - Drowning in Silence. +

+
+ +
+

Drowning in Silence - Beth

+

+ I’d never known silence like that before. The kind that creeps under your skin and settles in your bones, sinking in so deep it feels like it might smother you. When I opened the door, that silence wrapped itself around me, choking me, filling me up until there was nothing else. I didn’t even know what I was seeing at first. I think maybe my mind tried to protect me, tried to shield me from what was right in front of me, even though I knew, deep down, that everything was about to change. +

+

+ She was slumped there in the bath, water cold and still around her, her face as blank as a wax doll’s, skin washed out, lifeless. The first thought I had, the thing I’ll never forgive myself for, was how wrong it looked. It felt surreal, like a trick. This wasn’t her. It couldn’t be. My mum wasn’t a drinker, not like this, not ever, but there was an empty bottle lying on its side beside the bath, rolling slightly as I opened the door wider. It felt like it was mocking me, daring me to believe what I was seeing. +

+

+ I felt sick, my throat clenching, my stomach twisting, and for a moment, I hated her, or whoever had done this to her. Hated the absurdity, the impossibility of it. She’d never have chosen that bottle over me, over herself. And yet there it was, an empty accusation, staring at me from the floor, her face pale and her lips blue. I couldn’t make sense of it. I just stood there, a dead thing staring back at her, just as lifeless as she was. +

+

+ They say your life flashes before your eyes when you die, but I think they’re wrong. I think it’s the people left behind, the ones who have to see it, who have to stand there, watching their entire world collapse around them. I saw everything; all the tiny pieces of a life she’d held together for me, every smile, every reassuring word, every single thing that had kept me safe. And I realised, right then, that I was all alone. Utterly and completely alone. +

+

+ There’s something that breaks in you when you lose everything in one heartbeat. It’s like the walls inside you just give way, crumbling into nothing, until all that’s left is this empty shell. I felt it, that shattering, like glass splintering into a million pieces inside my chest. I remember gripping the doorframe so hard my knuckles turned white, the pain grounding me, keeping me from slipping into whatever dark pit was opening up beneath my feet. I couldn’t look away from her. I couldn’t move, couldn’t breathe. I was frozen, trapped in this nightmare that wouldn’t end, a part of me hoping that if I stared long enough, I’d wake up. That this would all just go away. +

+

+ But it didn’t. And I knew it wouldn’t. Because that was the moment my life ended too. She may have been the one in the water, but I was drowning right along with her... +

+
diff --git a/CatherineLynwood/Views/Discovery/Chapter13.cshtml b/CatherineLynwood/Views/Discovery/Chapter13.cshtml index d023874..5abb8b8 100644 --- a/CatherineLynwood/Views/Discovery/Chapter13.cshtml +++ b/CatherineLynwood/Views/Discovery/Chapter13.cshtml @@ -2,92 +2,94 @@ ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13"; } -
-
- -
-
- - -
-

Chapter 13 - A Name She Never Owned - Susie

-

An exclusive glimpse into Susie's story

-
- - -
- -
- - +
+
+
+ +
- -
-
- -
- -

- Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned -

-
+ +
+

Chapter 13 - A Name She Never Owned - Susie

+

An exclusive glimpse into Susie's story

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

+ Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned +

+
- -
-

A Name She Never Owned - Susie

-

I wasn’t sure this was a good idea. Not the date itself, hell, I deserved a decent night out for once, but who I was going with. I mean he’s a punter for God’s sake. That said, guys aren’t exactly queueing up to take me out, and he certainly seemed quite harmless, although perhaps a little needy. I didn’t feel threatened by him at all, and I’d definitely learned how to defend myself over the past three years, so I had no worries there.

-

My biggest problem was my wardrobe, if you can call it that. It wasn’t exactly huge, so I didn’t have many clothes to choose from. Having spent half my time in short skirts and stockings in the past three years, I decided to go for jeans and a pretty top. I had the nice one that Maggie gave me back at the beginning of the year that I hadn’t worn that much, and over the top I decided to wear my leather jacket. Yeah, that would do just perfect.

-

I looked at my watch. Nearly eight. I grabbed my handbag and pulled the door shut behind me, heading downstairs and out of the block. Opening the door to the street, the cold hit me like a smack in the face. Good job I wasn’t working tonight; I’d have frozen my tits off, I thought.

-

As I crossed the road, I saw Ben’s car waiting in the layby. He waved as I approached. I opened the door and got in.

-

“Hi,” he said, smiling nervously. “I wondered if you’d remember.”

-

“Of course,” I replied, returning his smile.

-

He leant over, I thought he was going to try and kiss me, and for a split second I panicked, reaching for the door handle, but instead he reached behind the seat, picking something up. He brought his hand around in front of me, presenting the most amazing bunch of red roses I had ever seen.

-

“These are for you.”

-

“Wow! Thank you,” I said, genuinely blown away. “That’s so nice of you. No one’s ever bought me flowers before.”

-

Ben’s smile deepened. “Are you ready?”

-

“Sure.”

-

He started the car and pulled away, driving slowly, a little nervously.

-

“Have you been driving long?” I asked after a few moments.

-

Ben glanced across. “About a year. Am I that bad?”

-

I laughed softly. “No, not at all. It’s just that most guys seem to drive a lot faster.”

-

“I don’t really like driving that much. It makes me a little nervous, but I have to do it… otherwise I would never get out of the house.”

-

“Do you live on your own?”

-

“Yes.” His expression darkened slightly. “My parents died last year… in a car crash. That’s why I’m nervous. I only drive because I have to.”

-

I reached over and rested my hand on his knee for a second, trying to offer some comfort. Ben gave my hand a gentle squeeze in acknowledgment.

-

“Listen,” he said, glancing at me again. “I don’t want this to be weird. So as far as I’m concerned, yesterday didn’t happen. We’re just on a date because… well, I like you. I think you’re gorgeous.”

-

“Thank you,” I said, feeling a blush creep up my cheeks.

-

“And I’m not expecting anything either,” he added quickly, looking flustered. “Sex wise, I mean. This is just two people going out for a drink. What either of us does for a living doesn’t matter. Okay?”

-

I smiled, warmed by his awkward honesty. “Suits me.”

-

We drove for about fifteen minutes, down a few country lanes and up a steep hill, until we came to a lovely country pub nestled into the hillside. Ben pulled into the car park and switched off the engine.

-

“This looks nice. Have you been here before?” I asked.

-

“No, never. A friend told me about it when I asked him where we could go.”

-

Shocked, I stared at him. “You told your mate you’re going on a date with me?”

-

“Yeah. What’s wrong with that?”

-

“Did you tell him I’m a prostitute?”

-

Ben gave a half-smile. “I thought we agreed what each of us does doesn’t matter?”

-

“It doesn’t.”

-

“Exactly. But if you must know, I just told him I had a date with a beautiful girl. That’s all.”

-

“So, you’re embarrassed to be seen with me?”

-

Ben looked flustered. “No, not at all. If I was, I wouldn’t have asked you out. I’ll shout it from the top of that hill over there if you like, but it won’t change the way I feel.”

-

I was quite touched.

-

We got out of the car and Ben locked the doors. The car park was surrounded by a low stone wall. Ben led the way over to some steps that wound up to the pub entrance above. When we reached the door, he held it open for me.

-

The pub was lovely. Very old-fashioned, with a big solid oak bar, a few assorted tables, and a mixture of traditional chairs and cosy lounge sofas scattered around. The scent of woodsmoke filled the air.

-

“Do you want to go and sit down, and I’ll get the drinks?” Ben asked, glancing at the bar.

-

“Yeah, okay. Can I have a vodka and Coke, please?”

-

I headed towards a sumptuous-looking sofa in the corner by the open fire and slumped down. It was beautifully soft, and for the first time in ages I started to relax. A thought flashed through my mind… maybe I should have dressed up more. Ben seemed nice. I felt a little underdressed now. Maybe next time, I thought.

-

It wasn’t long before Ben returned with the drinks. He placed mine carefully in front of me.

+ +
+

A Name She Never Owned - Susie

+

I wasn’t sure this was a good idea. Not the date itself, hell, I deserved a decent night out for once, but who I was going with. I mean he’s a punter for God’s sake. That said, guys aren’t exactly queueing up to take me out, and he certainly seemed quite harmless, although perhaps a little needy. I didn’t feel threatened by him at all, and I’d definitely learned how to defend myself over the past three years, so I had no worries there.

+

My biggest problem was my wardrobe, if you can call it that. It wasn’t exactly huge, so I didn’t have many clothes to choose from. Having spent half my time in short skirts and stockings in the past three years, I decided to go for jeans and a pretty top. I had the nice one that Maggie gave me back at the beginning of the year that I hadn’t worn that much, and over the top I decided to wear my leather jacket. Yeah, that would do just perfect.

+

I looked at my watch. Nearly eight. I grabbed my handbag and pulled the door shut behind me, heading downstairs and out of the block. Opening the door to the street, the cold hit me like a smack in the face. Good job I wasn’t working tonight; I’d have frozen my tits off, I thought.

+

As I crossed the road, I saw Ben’s car waiting in the layby. He waved as I approached. I opened the door and got in.

+

“Hi,” he said, smiling nervously. “I wondered if you’d remember.”

+

“Of course,” I replied, returning his smile.

+

He leant over, I thought he was going to try and kiss me, and for a split second I panicked, reaching for the door handle, but instead he reached behind the seat, picking something up. He brought his hand around in front of me, presenting the most amazing bunch of red roses I had ever seen.

+

“These are for you.”

+

“Wow! Thank you,” I said, genuinely blown away. “That’s so nice of you. No one’s ever bought me flowers before.”

+

Ben’s smile deepened. “Are you ready?”

+

“Sure.”

+

He started the car and pulled away, driving slowly, a little nervously.

+

“Have you been driving long?” I asked after a few moments.

+

Ben glanced across. “About a year. Am I that bad?”

+

I laughed softly. “No, not at all. It’s just that most guys seem to drive a lot faster.”

+

“I don’t really like driving that much. It makes me a little nervous, but I have to do it… otherwise I would never get out of the house.”

+

“Do you live on your own?”

+

“Yes.” His expression darkened slightly. “My parents died last year… in a car crash. That’s why I’m nervous. I only drive because I have to.”

+

I reached over and rested my hand on his knee for a second, trying to offer some comfort. Ben gave my hand a gentle squeeze in acknowledgment.

+

“Listen,” he said, glancing at me again. “I don’t want this to be weird. So as far as I’m concerned, yesterday didn’t happen. We’re just on a date because… well, I like you. I think you’re gorgeous.”

+

“Thank you,” I said, feeling a blush creep up my cheeks.

+

“And I’m not expecting anything either,” he added quickly, looking flustered. “Sex wise, I mean. This is just two people going out for a drink. What either of us does for a living doesn’t matter. Okay?”

+

I smiled, warmed by his awkward honesty. “Suits me.”

+

We drove for about fifteen minutes, down a few country lanes and up a steep hill, until we came to a lovely country pub nestled into the hillside. Ben pulled into the car park and switched off the engine.

+

“This looks nice. Have you been here before?” I asked.

+

“No, never. A friend told me about it when I asked him where we could go.”

+

Shocked, I stared at him. “You told your mate you’re going on a date with me?”

+

“Yeah. What’s wrong with that?”

+

“Did you tell him I’m a prostitute?”

+

Ben gave a half-smile. “I thought we agreed what each of us does doesn’t matter?”

+

“It doesn’t.”

+

“Exactly. But if you must know, I just told him I had a date with a beautiful girl. That’s all.”

+

“So, you’re embarrassed to be seen with me?”

+

Ben looked flustered. “No, not at all. If I was, I wouldn’t have asked you out. I’ll shout it from the top of that hill over there if you like, but it won’t change the way I feel.”

+

I was quite touched.

+

We got out of the car and Ben locked the doors. The car park was surrounded by a low stone wall. Ben led the way over to some steps that wound up to the pub entrance above. When we reached the door, he held it open for me.

+

The pub was lovely. Very old-fashioned, with a big solid oak bar, a few assorted tables, and a mixture of traditional chairs and cosy lounge sofas scattered around. The scent of woodsmoke filled the air.

+

“Do you want to go and sit down, and I’ll get the drinks?” Ben asked, glancing at the bar.

+

“Yeah, okay. Can I have a vodka and Coke, please?”

+

I headed towards a sumptuous-looking sofa in the corner by the open fire and slumped down. It was beautifully soft, and for the first time in ages I started to relax. A thought flashed through my mind… maybe I should have dressed up more. Ben seemed nice. I felt a little underdressed now. Maybe next time, I thought.

+

It wasn’t long before Ben returned with the drinks. He placed mine carefully in front of me.

+
diff --git a/CatherineLynwood/Views/Discovery/Chapter2.cshtml b/CatherineLynwood/Views/Discovery/Chapter2.cshtml index f1531a3..8dadbe5 100644 --- a/CatherineLynwood/Views/Discovery/Chapter2.cshtml +++ b/CatherineLynwood/Views/Discovery/Chapter2.cshtml @@ -2,75 +2,77 @@ ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2"; } -
-
- -
-
- - -
-

Chapter 2- The Last Lesson - Maggie

-

An exclusive glimpse into Maggie's story

-
- - -
- -
- - +
+
+
+ +
- -
-
- -
- -

- Listen to Maggie narrating the complete Chapter 2 - The Last Lesson, -

-
+ +
+

Chapter 2- The Last Lesson - Maggie

+

An exclusive glimpse into Maggie's story

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

+ Listen to Maggie narrating the complete Chapter 2 - The Last Lesson, +

+
- -
-

The Last Lesson - Maggie

-

There was a knock at the door.

-

“Colin’s here,” called mum.

-

“Okay, I’ll be there in a second,” I replied.

-

I was so nervous. I wasn’t normally the nervous type, but today was important. Who would book their driving test on Christmas Eve for God’s sake, I must be crazy. It’s going to be manic out there, I thought to myself, trying to stay calm.

-

I checked my look in the mirror: Hair – yes perfect, makeup – spot on, my shirt – white and business like, possibly a bit thin but necessarily so, skirt – perfect length, stockings – I always wore stockings, shoes – practical. Okay let’s do this, I thought.

-

“Maggie,” called Mum, “you’re going to be late.”

-

“Coming.”

-

I walked into the hallway where mum was talking to Colin.

-

“How do I look?” I asked.

-

Mum looked at me. “No bra?” she queried.

-

I just looked at her, and mum smiled in response.

-

Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.

-

“Good luck, Sweetie,” said Mum.

-

Colin led the way to the car. I don’t know why, it’s not like it was my first lesson. Hopefully, it was my last. We arrived at the car and even more bizarrely he opened the driver’s door for me.

-

“Thank you,” I said. “What’s with the chivalry?”

-

“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.

-

I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.

-

“Just take your time,” said Colin.

-

As if totally ignoring him, I revved the engine far too fast and slipped my foot off the clutch. The car leapt backwards in a tight right-hand arc.

-

“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.

-

“Sorry, I’m rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.

-

Colin looked at me closely, clearly wondering why the cool, calm, and collected girl he’d been teaching to drive for the past four months was suddenly driving like a complete idiot. I didn’t know what had come over me. I had been waiting for this day for so long and was so ready, but for some reason I was shaking. Pull yourself together, I thought.

-

Calmly, he said, “Don’t worry. Let’s just try that again...”

+ +
+

The Last Lesson - Maggie

+

There was a knock at the door.

+

“Colin’s here,” called mum.

+

“Okay, I’ll be there in a second,” I replied.

+

I was so nervous. I wasn’t normally the nervous type, but today was important. Who would book their driving test on Christmas Eve for God’s sake, I must be crazy. It’s going to be manic out there, I thought to myself, trying to stay calm.

+

I checked my look in the mirror: Hair – yes perfect, makeup – spot on, my shirt – white and business like, possibly a bit thin but necessarily so, skirt – perfect length, stockings – I always wore stockings, shoes – practical. Okay let’s do this, I thought.

+

“Maggie,” called Mum, “you’re going to be late.”

+

“Coming.”

+

I walked into the hallway where mum was talking to Colin.

+

“How do I look?” I asked.

+

Mum looked at me. “No bra?” she queried.

+

I just looked at her, and mum smiled in response.

+

Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.

+

“Good luck, Sweetie,” said Mum.

+

Colin led the way to the car. I don’t know why, it’s not like it was my first lesson. Hopefully, it was my last. We arrived at the car and even more bizarrely he opened the driver’s door for me.

+

“Thank you,” I said. “What’s with the chivalry?”

+

“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.

+

I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.

+

“Just take your time,” said Colin.

+

As if totally ignoring him, I revved the engine far too fast and slipped my foot off the clutch. The car leapt backwards in a tight right-hand arc.

+

“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.

+

“Sorry, I’m rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.

+

Colin looked at me closely, clearly wondering why the cool, calm, and collected girl he’d been teaching to drive for the past four months was suddenly driving like a complete idiot. I didn’t know what had come over me. I had been waiting for this day for so long and was so ready, but for some reason I was shaking. Pull yourself together, I thought.

+

Calmly, he said, “Don’t worry. Let’s just try that again...”

+
diff --git a/CatherineLynwood/Views/Discovery/IndexDesktop.cshtml b/CatherineLynwood/Views/Discovery/IndexDesktop.cshtml index f705311..0035f27 100644 --- a/CatherineLynwood/Views/Discovery/IndexDesktop.cshtml +++ b/CatherineLynwood/Views/Discovery/IndexDesktop.cshtml @@ -1,4 +1,4 @@ -@model CatherineLynwood.Models.DiscoveryPageViewModel +@model CatherineLynwood.Models.TitlePageViewModel @{ ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters"; @@ -64,18 +64,7 @@ } - - @* buyBox: server-side slugs + tracking *@ - @* Model: CatherineLynwood.Models.DiscoveryPageViewModel *@ - @{ - var L = Model.Buy; - string pingBase = "/track/click"; - string countryIso2 = Model.UserIso2 ?? "GB"; - string flagPathSvg = $"/images/flags/{countryIso2}.svg"; - string flagPathPng = $"/images/flags/{countryIso2}.png"; - } - - + @await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
@@ -83,6 +72,28 @@
+
+ +
+ + @if (showReviews) { @@ -144,7 +155,7 @@

The Alpha Flame: Discovery

-

Survival, secrets, and sisters in 1983 Birmingham.

+

Survival, secrets, and shadows in 1983 Birmingham.

@@ -165,6 +176,12 @@
+

+ She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away. +

+

+ But nothing prepares her for Beth. As she digs deeper into Beth’s world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isn’t someone else’s nightmare. It’s her own. +

Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken.

diff --git a/CatherineLynwood/Views/Discovery/IndexMobileA.cshtml b/CatherineLynwood/Views/Discovery/IndexMobileA.cshtml index 010347f..88c1449 100644 --- a/CatherineLynwood/Views/Discovery/IndexMobileA.cshtml +++ b/CatherineLynwood/Views/Discovery/IndexMobileA.cshtml @@ -1,4 +1,4 @@ -@model CatherineLynwood.Models.DiscoveryPageViewModel +@model CatherineLynwood.Models.TitlePageViewModel @{ ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters"; @@ -19,6 +19,12 @@
+
+
+

The Alpha Flame: Discovery

+

Birmingham 1983 - A tale of survival, secrets, and sisterhood.

+
+
@@ -77,6 +72,27 @@
+
+ +
+ @if (showReviews) { @@ -136,46 +152,44 @@
-
-

The Alpha Flame: Discovery

-

Survival, secrets, and sisters in 1983 Birmingham.

+
+

The Alpha Flame: Discovery

+

Birmingham 1983 - A tale of survival, secrets, and sisterhood.

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

Listen to Catherine talking about the book

+
+
+ + +
+
+

She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away.

+

But nothing prepares her for Beth. As she digs deeper into Beth’s world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isn’t someone else’s nightmare. It’s her own.

+

Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken.

+

For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beth’s existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.

+

Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesn’t just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isn’t just a car; it’s an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggie’s intensity doesn’t stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isn’t a mistake anyone makes twice.

+

When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability she’s rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.

+

Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines’ journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.

+

As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.

+

At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It captures the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final page.

+
+
+
- -
- -
-
- -
-
-
- -
-

Listen to Catherine talking about the book

-
-
- - -

- Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken. -

- - -
-
-

For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beth’s existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.

-

Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesn’t just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isn’t just a car; it’s an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggie’s intensity doesn’t stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isn’t a mistake anyone makes twice.

-

When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability she’s rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.

-

Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high-speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines’ journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.

-

As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.

-

At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It captures the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final page.

-
-
-
-
diff --git a/CatherineLynwood/Views/Discovery/IndexMobileB.cshtml b/CatherineLynwood/Views/Discovery/IndexMobileB.cshtml index 7aa5d21..679ab94 100644 --- a/CatherineLynwood/Views/Discovery/IndexMobileB.cshtml +++ b/CatherineLynwood/Views/Discovery/IndexMobileB.cshtml @@ -1,4 +1,4 @@ -@model CatherineLynwood.Models.DiscoveryPageViewModel +@model CatherineLynwood.Models.TitlePageViewModel @{ ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters"; @@ -55,6 +55,27 @@
+
+
+
+

★ Audiobook Out Now ★

+ +

+ The Alpha Flame: Discovery is now available as a full-length audiobook, narrated to be lived in rather than rushed through. + It’s a slow-burn, character-led story where quiet moments matter, tension builds gradually, and certain scenes land harder when you hear them spoken. +

+ +

+ Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen. +

+ + + 🎧 Explore the audiobook + +
+
+
+
@@ -80,14 +101,12 @@
- -

- Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken. -

- - +
+

She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away.

+

But nothing prepares her for Beth. As she digs deeper into Beth’s world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isn’t someone else’s nightmare. It’s her own.

+

Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows the lives of two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken.

For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beth’s existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.

Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesn’t just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isn’t just a car; it’s an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggie’s intensity doesn’t stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isn’t a mistake anyone makes twice.

When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability she’s rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.

@@ -161,17 +180,7 @@ diff --git a/CatherineLynwood/Views/Discovery/IndexMobileC.cshtml b/CatherineLynwood/Views/Discovery/IndexMobileC.cshtml deleted file mode 100644 index ac15614..0000000 --- a/CatherineLynwood/Views/Discovery/IndexMobileC.cshtml +++ /dev/null @@ -1,605 +0,0 @@ -@model CatherineLynwood.Models.DiscoveryPageViewModel - -@{ - ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters"; - bool showReviews = Model.Reviews.Items.Any(); -} - - - - -
-
-
- - - - - - - - -
- - -
-
- - - - - - - - -
-

The story starts here.

- Buy now -
-
-
- - -
-
-
-

The Alpha Flame: Discovery

-

Survival, secrets, and sisters in 1983 Birmingham.

-
- -
- -
-
- -
-
-
- -
-

Listen to Catherine talking about the book

-
-
- - -

- Set in 1983 Birmingham, nearby Redditch, and Barmouth in Wales, The Alpha Flame: Discovery follows two young women, Beth and Maggie, as they uncover dark family secrets and fight to survive. Gritty and emotionally charged, it explores the bond between two women who refuse to be broken. -

- - -
-
-

For Beth, the world is a cold and unforgiving place. Devastation strikes in a single moment, leaving her isolated, shattered, and vulnerable. Alone in the bleak shadows of a city that offers neither refuge nor redemption, she is forced to navigate a relentless cycle of desperation and despair. Every step of her journey tests the limits of her endurance, pushing her into harrowing situations where survival feels like a hollow victory. Beth’s existence is marked by loss, betrayal, and an almost suffocating loneliness that threatens to consume her entirely. Yet, even in the darkest corners of her ordeal, a fragile ember of defiance smoulders within her, a quiet, stubborn refusal to let the world destroy her completely.

-

Maggie, by contrast, is a force of nature, a woman who thrives on her unshakable drive and an unrelenting belief in her own power. Behind her fiery red hair and disarming charm lies a storm of determination and ferocity. Maggie doesn’t just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isn’t just a car; it’s an extension of her spirit, sleek, powerful, and unapologetically bold. On the open road, with the engine roaring and the world blurring past her, she feels invincible. But Maggie’s intensity doesn’t stop at the wheel. Her relationships burn just as brightly. As a lover, she is dominant, passionate, and unafraid to embrace her darker desires. While fiercely loving and loyal, Maggie is also formidable; crossing her isn’t a mistake anyone makes twice.

-

When fate brings Beth and Maggie together, their connection is explosive, a union of two polar opposites that burns with both tenderness and raw power. For Beth, Maggie represents a lifeline, a reminder that love and trust still exist, even in a world that has betrayed her at every turn. For Maggie, Beth awakens a fierce protectiveness and vulnerability she’s rarely allowed herself to feel. Together, they ignite a flame that challenges them to confront their own fears, desires, and limitations.

-

Set against the kaleidoscope of 1983, where synthesised anthems provide a pulsing soundtrack and the streets are alive with the bold styles and rebellious energy of the decade, their story unfolds in a city teeming with danger and intrigue. From high speed chases along winding roads to dimly lit clubs and desolate alleyways, the heroines’ journey is a visceral exploration of survival and freedom. The neon haze of the era contrasts sharply with the stark realities they face, painting a vivid picture of a world where strength and vulnerability coexist.

-

As secrets surface and danger tightens its grip, Beth and Maggie must confront not only the challenges around them but the truths within themselves. Their bond is tested by betrayal, desire, and the shadows of their pasts, but through it all, their flame burns brighter, illuminating their courage and the unbreakable spirit of two heroines determined to rewrite their fates.

-

At its heart, The Alpha Flame is a story of survival, passion, and empowerment. It captures the raw power of human connection against the gritty, vibrant backdrop of an unforgettable era. With its blend of drama, intensity, and unapologetic emotion, this is a story that will leave its mark long after the final page.

-
-
-
-
-
- - -@if (showReviews) -{ - var top = Model.Reviews.Items.Where(x => x.RatingValue == 5).OrderByDescending(y => y.DatePublished).First(); - var fullStars = (int)Math.Floor(top.RatingValue); - var hasHalfStar = top.RatingValue - fullStars >= 0.5; - var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); - var reviewDate = top.DatePublished.ToString("d MMMM yyyy"); - -
-
-
-

★ Reader Praise ★

-
- - @for (int i = 0; i < fullStars; i++) - { - - } - @if (hasHalfStar) - { - - } - @for (int i = 0; i < emptyStars; i++) - { - - } - - @Html.Raw(top.ReviewBody) -
- @top.AuthorName on - - @if (string.IsNullOrEmpty(top.URL)) - { - @top.SiteName - } - else - { - @top.SiteName - } - - , @reviewDate -
-
- @if (Model.Reviews.Items.Count > 1) - { - - } -
-
-
-} - - -
- -
- - - - - -
-

Chapter Previews

-
-
-
- - - -
-

Chapter 1, Drowning in Silence, Beth

-

Beth returns home to find her mother lifeless in the bath...

- -
-
-
-
-
- - - -
-

Chapter 2, The Last Lesson, Maggie

-

On Christmas Eve, Maggie nervously heads out for her driving test not knowing the story that will pan out before her...

- -
-
-
-
-
- - - -
-

Chapter 13, A Name She Never Owned, Susie

-

Susie goes out for a drink with a punter. What on earth could go wrong...

- -
-
-
-
-
- -@section Scripts { - - - - - - - -} - -@section Meta { - - - -} diff --git a/CatherineLynwood/Views/Discovery/_BuyBox.cshtml b/CatherineLynwood/Views/Discovery/_BuyBox.cshtml deleted file mode 100644 index cf6e5cf..0000000 --- a/CatherineLynwood/Views/Discovery/_BuyBox.cshtml +++ /dev/null @@ -1,172 +0,0 @@ -@model CatherineLynwood.Models.DiscoveryPageViewModel -@{ - var L = Model?.Buy ?? new CatherineLynwood.Models.BuyLinksViewModel(); - var iso2 = (Model?.UserIso2 ?? "GB").ToUpperInvariant(); - if (iso2 == "UK") { iso2 = "GB"; } - var pingBase = "/track/click"; - var flagSvg = $"/images/flags/{iso2}.svg"; - var flagPng = $"/images/flags/{iso2}.png"; -} - -
- -
-

Buy the Book

- - - Best options for @iso2 - -
- - @* --------------------------- - Row 1: Direct via printers (GB/US only) - --------------------------- *@ - @if (L.IngramHardback != null || L.IngramPaperback != null) - { - - } - -
- From other retailers -
- - @* --------------------------- - Row 2: Amazon (always present) - --------------------------- *@ - - - @* --------------------------- - Row 3: National retailer (conditional) - --------------------------- *@ - @if (L.NationalHardback != null || L.NationalPaperback != null) - { -
-
- @if (L.NationalHardback != null) - { - - } - @if (L.NationalPaperback != null) - { - - } -
-
- } - - @* --------------------------- - Row 4: eBook stores (Apple, Kobo, Kindle) - --------------------------- *@ -
-
- Choose your preferred e-book store -
- -
- - -
diff --git a/CatherineLynwood/Views/Discovery/_Layout.cshtml b/CatherineLynwood/Views/Discovery/_Layout.cshtml new file mode 100644 index 0000000..4dc0194 --- /dev/null +++ b/CatherineLynwood/Views/Discovery/_Layout.cshtml @@ -0,0 +1,39 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} + +@section CSS{ + + +} + +@section Meta { + @RenderSection("Meta", required: false) +} + + +@section BackgroundVideo { +
+
+ +
+
+
+} + +@RenderBody() + +@section Scripts { + @RenderSection("Scripts", required: false) +} diff --git a/CatherineLynwood/Views/Home/Index.cshtml b/CatherineLynwood/Views/Home/Index.cshtml index f6867fd..d40ba3d 100644 --- a/CatherineLynwood/Views/Home/Index.cshtml +++ b/CatherineLynwood/Views/Home/Index.cshtml @@ -12,17 +12,16 @@
- -
+
- +
-
-

The Alpha Flame: Discovery

-

Some girls survive. Others set the world on fire.

-

- When Maggie Grant finds Beth, bruised, terrified, and alone, she has no idea the rescue will ignite a chain of secrets buried deep in her own past... + +

+ The Alpha Flame + A Trilogy by Catherine Lynwood +

+ +

+ Some girls survive. Others set the world on fire.

-
- Explore the Book + +

+ Maggie Grant never meant to save anyone. But when she finds Beth bruised, terrified, and running from something unspeakable, walking away is no longer an option. + What begins as a single act of kindness pulls them into a world of buried secrets, quiet violence, and truths that refuse to stay hidden. +

+ +
+ +

+ The Alpha Flame: Discovery +

+ +

+ The first novel introduces Maggie and Beth and the fragile, dangerous bond that forms between them. + Set against 1980s Birmingham, Discovery is a story of survival, loyalty, and the moment a life quietly changes forever. +

+ + -

-

The Alpha Flame: Discovery

, writen by Catherine Lynwood, is the first in a powerful trilogy following the tangled lives of Maggie and Beth, two women bound by fate, fire, and secrets too dangerous to stay buried. - The journey continues in Reckoning (Spring 2026) and concludes with Redemption (Autumn 2026). - Learn more about the full trilogy on the Alpha Flame series page. + +

+ The Alpha Flame: Reckoning +

+ +

+ As pasts collide and consequences close in, Maggie and Beth are forced to confront the truth of who they are, and what they are willing to become. + Reckoning deepens the danger, raises the stakes, and proves that survival always comes at a cost.

+ + + +

+ The Alpha Flame, written by + Catherine Lynwood, + is a powerful trilogy of UK historical fiction following the intertwined lives of Maggie and Beth. + The story begins with Discovery, intensifies in Reckoning (Spring 2026), + and concludes with Redemption (Autumn 2026). +

+ @if (DateTime.Now < new DateTime(2025, 9, 1)) { -

-

Win: a Collector’s Edition of The Alpha Flame: Discovery

- Exclusive, limited, beautiful. +
+ +

+

+ Win: + A Collector’s Edition of The Alpha Flame: Discovery +

+ Exclusive. Limited. Beautiful.

+ } +

-

About The Alpha Flame Trilogy

, is gripping UK historical fiction set in 1980s Birmingham. These novels combine family drama, dark secrets, and emotional suspense as two sisters fight for truth and redemption. Perfect for readers who enjoy tense, character-driven stories and literary suspense in a richly described real-world setting. +

About the Trilogy

+ Set in 1980s Birmingham, The Alpha Flame blends family drama, emotional suspense, + and dark secrets rooted in real places and lived experience. + Ideal for readers who value character-driven storytelling, moral complexity, + and stories that linger long after the final page.

+
+
@@ -192,7 +245,7 @@ setTimeout(() => { const video = document.getElementById("heroVideo"); const source = document.createElement("source"); - source.src = "/videos/background-5.mp4"; + source.src = "/videos/the-alpha-flame-reckoning.mp4"; source.type = "video/mp4"; video.appendChild(source); video.load(); // Triggers actual file load diff --git a/CatherineLynwood/Views/Reckoning/Chapter1.cshtml b/CatherineLynwood/Views/Reckoning/Chapter1.cshtml new file mode 100644 index 0000000..84ec506 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Chapter1.cshtml @@ -0,0 +1,157 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1"; +} +
+
+
+ +
+
+ + +
+

Chapter 1 - Drowning in Silence - Beth

+

An exclusive glimpse into Beth's story

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

+ Watch Beth narrating part of Chapter 1 - Drowning in Silence. +

+ +
+ +

+ Listen to Beth narrating the complete Chapter 1 - Drowning in Silence. +

+
+ +
+

Drowning in Silence - Beth

+

+ I’d never known silence like that before. The kind that creeps under your skin and settles in your bones, sinking in so deep it feels like it might smother you. When I opened the door, that silence wrapped itself around me, choking me, filling me up until there was nothing else. I didn’t even know what I was seeing at first. I think maybe my mind tried to protect me, tried to shield me from what was right in front of me, even though I knew, deep down, that everything was about to change. +

+

+ She was slumped there in the bath, water cold and still around her, her face as blank as a wax doll’s, skin washed out, lifeless. The first thought I had, the thing I’ll never forgive myself for, was how wrong it looked. It felt surreal, like a trick. This wasn’t her. It couldn’t be. My mum wasn’t a drinker, not like this, not ever, but there was an empty bottle lying on its side beside the bath, rolling slightly as I opened the door wider. It felt like it was mocking me, daring me to believe what I was seeing. +

+

+ I felt sick, my throat clenching, my stomach twisting, and for a moment, I hated her, or whoever had done this to her. Hated the absurdity, the impossibility of it. She’d never have chosen that bottle over me, over herself. And yet there it was, an empty accusation, staring at me from the floor, her face pale and her lips blue. I couldn’t make sense of it. I just stood there, a dead thing staring back at her, just as lifeless as she was. +

+

+ They say your life flashes before your eyes when you die, but I think they’re wrong. I think it’s the people left behind, the ones who have to see it, who have to stand there, watching their entire world collapse around them. I saw everything; all the tiny pieces of a life she’d held together for me, every smile, every reassuring word, every single thing that had kept me safe. And I realised, right then, that I was all alone. Utterly and completely alone. +

+

+ There’s something that breaks in you when you lose everything in one heartbeat. It’s like the walls inside you just give way, crumbling into nothing, until all that’s left is this empty shell. I felt it, that shattering, like glass splintering into a million pieces inside my chest. I remember gripping the doorframe so hard my knuckles turned white, the pain grounding me, keeping me from slipping into whatever dark pit was opening up beneath my feet. I couldn’t look away from her. I couldn’t move, couldn’t breathe. I was frozen, trapped in this nightmare that wouldn’t end, a part of me hoping that if I stared long enough, I’d wake up. That this would all just go away. +

+

+ But it didn’t. And I knew it wouldn’t. Because that was the moment my life ended too. She may have been the one in the water, but I was drowning right along with her... +

+
+
+
+
+
+ +@section Scripts{ + +} + +@section Meta { + + + + + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/Chapter13.cshtml b/CatherineLynwood/Views/Reckoning/Chapter13.cshtml new file mode 100644 index 0000000..5abb8b8 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Chapter13.cshtml @@ -0,0 +1,173 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13"; +} + +
+
+
+ +
+
+ + +
+

Chapter 13 - A Name She Never Owned - Susie

+

An exclusive glimpse into Susie's story

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

+ Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned +

+
+ + + +
+

A Name She Never Owned - Susie

+

I wasn’t sure this was a good idea. Not the date itself, hell, I deserved a decent night out for once, but who I was going with. I mean he’s a punter for God’s sake. That said, guys aren’t exactly queueing up to take me out, and he certainly seemed quite harmless, although perhaps a little needy. I didn’t feel threatened by him at all, and I’d definitely learned how to defend myself over the past three years, so I had no worries there.

+

My biggest problem was my wardrobe, if you can call it that. It wasn’t exactly huge, so I didn’t have many clothes to choose from. Having spent half my time in short skirts and stockings in the past three years, I decided to go for jeans and a pretty top. I had the nice one that Maggie gave me back at the beginning of the year that I hadn’t worn that much, and over the top I decided to wear my leather jacket. Yeah, that would do just perfect.

+

I looked at my watch. Nearly eight. I grabbed my handbag and pulled the door shut behind me, heading downstairs and out of the block. Opening the door to the street, the cold hit me like a smack in the face. Good job I wasn’t working tonight; I’d have frozen my tits off, I thought.

+

As I crossed the road, I saw Ben’s car waiting in the layby. He waved as I approached. I opened the door and got in.

+

“Hi,” he said, smiling nervously. “I wondered if you’d remember.”

+

“Of course,” I replied, returning his smile.

+

He leant over, I thought he was going to try and kiss me, and for a split second I panicked, reaching for the door handle, but instead he reached behind the seat, picking something up. He brought his hand around in front of me, presenting the most amazing bunch of red roses I had ever seen.

+

“These are for you.”

+

“Wow! Thank you,” I said, genuinely blown away. “That’s so nice of you. No one’s ever bought me flowers before.”

+

Ben’s smile deepened. “Are you ready?”

+

“Sure.”

+

He started the car and pulled away, driving slowly, a little nervously.

+

“Have you been driving long?” I asked after a few moments.

+

Ben glanced across. “About a year. Am I that bad?”

+

I laughed softly. “No, not at all. It’s just that most guys seem to drive a lot faster.”

+

“I don’t really like driving that much. It makes me a little nervous, but I have to do it… otherwise I would never get out of the house.”

+

“Do you live on your own?”

+

“Yes.” His expression darkened slightly. “My parents died last year… in a car crash. That’s why I’m nervous. I only drive because I have to.”

+

I reached over and rested my hand on his knee for a second, trying to offer some comfort. Ben gave my hand a gentle squeeze in acknowledgment.

+

“Listen,” he said, glancing at me again. “I don’t want this to be weird. So as far as I’m concerned, yesterday didn’t happen. We’re just on a date because… well, I like you. I think you’re gorgeous.”

+

“Thank you,” I said, feeling a blush creep up my cheeks.

+

“And I’m not expecting anything either,” he added quickly, looking flustered. “Sex wise, I mean. This is just two people going out for a drink. What either of us does for a living doesn’t matter. Okay?”

+

I smiled, warmed by his awkward honesty. “Suits me.”

+

We drove for about fifteen minutes, down a few country lanes and up a steep hill, until we came to a lovely country pub nestled into the hillside. Ben pulled into the car park and switched off the engine.

+

“This looks nice. Have you been here before?” I asked.

+

“No, never. A friend told me about it when I asked him where we could go.”

+

Shocked, I stared at him. “You told your mate you’re going on a date with me?”

+

“Yeah. What’s wrong with that?”

+

“Did you tell him I’m a prostitute?”

+

Ben gave a half-smile. “I thought we agreed what each of us does doesn’t matter?”

+

“It doesn’t.”

+

“Exactly. But if you must know, I just told him I had a date with a beautiful girl. That’s all.”

+

“So, you’re embarrassed to be seen with me?”

+

Ben looked flustered. “No, not at all. If I was, I wouldn’t have asked you out. I’ll shout it from the top of that hill over there if you like, but it won’t change the way I feel.”

+

I was quite touched.

+

We got out of the car and Ben locked the doors. The car park was surrounded by a low stone wall. Ben led the way over to some steps that wound up to the pub entrance above. When we reached the door, he held it open for me.

+

The pub was lovely. Very old-fashioned, with a big solid oak bar, a few assorted tables, and a mixture of traditional chairs and cosy lounge sofas scattered around. The scent of woodsmoke filled the air.

+

“Do you want to go and sit down, and I’ll get the drinks?” Ben asked, glancing at the bar.

+

“Yeah, okay. Can I have a vodka and Coke, please?”

+

I headed towards a sumptuous-looking sofa in the corner by the open fire and slumped down. It was beautifully soft, and for the first time in ages I started to relax. A thought flashed through my mind… maybe I should have dressed up more. Ben seemed nice. I felt a little underdressed now. Maybe next time, I thought.

+

It wasn’t long before Ben returned with the drinks. He placed mine carefully in front of me.

+
+
+
+
+
+ +@section Scripts{ + +} + +@section Meta { + + + + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/Chapter2.cshtml b/CatherineLynwood/Views/Reckoning/Chapter2.cshtml new file mode 100644 index 0000000..8dadbe5 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Chapter2.cshtml @@ -0,0 +1,156 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2"; +} + +
+
+
+ +
+
+ + +
+

Chapter 2- The Last Lesson - Maggie

+

An exclusive glimpse into Maggie's story

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

+ Listen to Maggie narrating the complete Chapter 2 - The Last Lesson, +

+
+ + + +
+

The Last Lesson - Maggie

+

There was a knock at the door.

+

“Colin’s here,” called mum.

+

“Okay, I’ll be there in a second,” I replied.

+

I was so nervous. I wasn’t normally the nervous type, but today was important. Who would book their driving test on Christmas Eve for God’s sake, I must be crazy. It’s going to be manic out there, I thought to myself, trying to stay calm.

+

I checked my look in the mirror: Hair – yes perfect, makeup – spot on, my shirt – white and business like, possibly a bit thin but necessarily so, skirt – perfect length, stockings – I always wore stockings, shoes – practical. Okay let’s do this, I thought.

+

“Maggie,” called Mum, “you’re going to be late.”

+

“Coming.”

+

I walked into the hallway where mum was talking to Colin.

+

“How do I look?” I asked.

+

Mum looked at me. “No bra?” she queried.

+

I just looked at her, and mum smiled in response.

+

Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.

+

“Good luck, Sweetie,” said Mum.

+

Colin led the way to the car. I don’t know why, it’s not like it was my first lesson. Hopefully, it was my last. We arrived at the car and even more bizarrely he opened the driver’s door for me.

+

“Thank you,” I said. “What’s with the chivalry?”

+

“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.

+

I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.

+

“Just take your time,” said Colin.

+

As if totally ignoring him, I revved the engine far too fast and slipped my foot off the clutch. The car leapt backwards in a tight right-hand arc.

+

“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.

+

“Sorry, I’m rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.

+

Colin looked at me closely, clearly wondering why the cool, calm, and collected girl he’d been teaching to drive for the past four months was suddenly driving like a complete idiot. I didn’t know what had come over me. I had been waiting for this day for so long and was so ready, but for some reason I was shaking. Pull yourself together, I thought.

+

Calmly, he said, “Don’t worry. Let’s just try that again...”

+ +
+
+
+
+
+ +@section Scripts{ + +} + +@section Meta { + + + + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/Epilogue.cshtml b/CatherineLynwood/Views/Reckoning/Epilogue.cshtml new file mode 100644 index 0000000..044de26 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Epilogue.cshtml @@ -0,0 +1,160 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Epilogue"; +} + +
+
+ +
+
+ + +
+

The Alpha Flame: Discovery

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

Epilogue

+

Narrated by Maggie – Late May 1983

+
+ +
+

Beth and I pulled into the car park of the Barnt Green Inn with the roof down, the late afternoon sun warming our faces. Everything looked more vivid lately, the colours brighter, the air lighter. It was like the whole world had released a breath.

+

I locked the car, and we wandered round to the beer garden. Rob and Zoe were already there, sat close, chatting like old friends. Even now, after everything, it still surprised me to see them so relaxed together.

+

Rob spotted us and raised a hand. “Hey!”

+

I leaned down and kissed him on the cheek. “Hiya. Can you get us a drink?”

+

“Of course. What do you fancy?”

+

“Coke for me. Beth?”

+

She gave a small smile. “Cider, please.”

+

Rob nodded and headed for the bar.

+

Beth and I took the seats opposite Zoe.

+

“How’ve you both been?” Zoe asked, giving us a once-over.

+

I glanced at Beth. “Good… I think. Still getting used to it all.”

+

Beth nodded. “Yeah. I’m alright. Put some weight on. I’m nearly as fat as Maggie now,” she grinned.

+

I laughed, nudging her. “Nearly? That ship has sailed, love.”

+

“Mum’s fault. Too many sausage sandwiches.”

+

“You both look good,” Zoe said, eyes lingering just a little longer on Beth. “Bruises gone?”

+

We nodded.

+

“So, what have you two been doing with yourselves?”

+

Beth perked up. “Helping Maggie with her designs. Turns out I’m not bad with a needle.”

+

“And she’s not a bad model either,” I added. “Definitely helps having someone try things on.”

+

Zoe smirked. “Do you make her twirl like they do on those fashion shows?”

+

Beth put on a mock pout. “She makes me pose like I’m in Vogue. Arms here, head there.”

+

“Only when you’re being annoying,” I said, laughing. “Besides, you love it.”

+

“True,” Beth admitted. “It’s actually fun. I never thought I’d enjoy something like that.”

+

Laughter echoed behind us as Rosie and Rebecca came round the corner.

+

“She’s deadly,” Rebecca giggled. “I had my eyes closed the whole way here.”

+

“It wasn’t that bad!” Rosie huffed, trying to look offended. “Not my fault people kept jumping out in front of me.”

+

“You might try braking instead of shouting. It’s far more effective,” Rebecca teased.

+

“Where’s Rob?” Rosie asked.

+

“He’s at the bar. If you’re quick, you might get a free drink,” I said.

+

Right on cue, Rob returned with a tray.

+

He clocked the new arrivals and sighed. “I’m going back to the bar, aren’t I?”

+

He placed the drinks down, then turned with theatrical weariness. “The usual, girls?”

+

“Yes please,” said Rebecca sweetly.

+

Once he was back and settled, we all relaxed into the rhythm of easy chatter.

+

“So, who’s got summer plans then?” Rosie asked. “Please tell me someone’s going somewhere glamorous.”

+

“Barnt Green counts as glamorous, right?” I said.

+

“Only if you squint,” said Zoe. “And ignore the smell of wet dog that always seems to hang around the canal.”

+

“I might take Beth to London,” I said. “Show her the big city.”

+

Beth raised an eyebrow. “What for?”

+

“So, you can be horrified at the price of everything and then come home saying how much nicer Birmingham is.”

+

“Sold,” she laughed.

+

Zoe leaned forward, lowering her voice slightly. “I heard from Graham. The case against Rick’s progressing.”

+

Beth stiffened slightly but nodded.

+

“Are you two going to be alright when it comes to court?”

+

Beth and I exchanged a look. We’d talked about it endlessly in private.

+

“We think so,” Beth said. “It’s black and white for us. We just have to tell the truth.”

+

Rob had gone quiet, his brow furrowed.

+

“You alright?” I asked.

+

He hesitated, then said, “It’s just… I don’t believe in coincidences. Not ones like that.”

+

Rebecca cocked her head. “Is he always this cryptic?”

+

“Most of the time,” I said. “This one’s new, though. Go on, Rob. Spit it out.”

+

“Rick,” he said. “The night Beth ended up under the flyover. Him just being there… it never sat right with me.”

+

Beth’s face fell still. “You think he was following me?”

+

Rob gave a half-shrug. “Feels too neat to be chance.”

+

Beth looked down. “He did have the flat, though. Cindy lived there too, remember. Maybe he was nearby anyway.”

+

“Maybe,” Rob said, but not like he believed it.

+

Rosie clapped her hands lightly. “Well, on a brighter note, Greg’s birthday is next week. He’s throwing a party. You’re all invited.”

+

Beth’s eyes lit up. “I’ve never been to a grown-up party. Not really. I’m in.”

+

“You’ll need something to wear,” I said. “And no, you’re not stealing that red skirt again.”

+

“I only borrowed it.”

+

“For three weeks.”

+

Rosie smirked. “Are you two dressing the same again?”

+

“Obviously,” we said in unison.

+

Zoe groaned. “Have you noticed they do that constantly now?” She turned to Rob. “Seriously, get out while you still can.”

+

Beth and I grinned at each other.

+

“I really don’t know what they’re…”

+

“…on about. Do you?” we said, still in sync.

+

Rebecca had been quiet, but now her voice was cautious. “Are you going to look for your dad?”

+

We both paused.

+

“We would like to,” I replied. “We haven’t really got too much to go on. There are a few things we’ve got to follow up in Beth’s memory tin. There’s a bank book for one. It’s only got one pound seventy-nine in it, but it might give us a clue.”

+

Beth added. “We’ve also got the wedding photos… and a date. Oh, and even the name of the church. So, we might be able to find something there, if we can find the right church.”

+

The table fell quiet for a moment.

+

“Did your mum ever talk about him?” Rebecca asked.

+

“Not really,” Beth said. “She used to say ‘He’s nobody worth knowing.’ But I’m not sure she meant it.”

+

Rosie glanced up. “It’s such a lovely day. We should go to the beach one weekend.”

+

Beth’s eyes went wide. “Yes! All of us. It’d be amazing.”

+

“I’ll bring sandwiches,” said Rob. “Beth, you’re banned from cooking anything though.”

+

“Hey! That cheese toastie was only slightly burnt.”

+

“It was charcoal.”

+

“I quite liked it,” I added. “Adds crunch.”

+

General agreement followed.

+

“I need the loo,” said Rebecca.

+

“Me too. I’ll grab drinks on the way back. Same again?”

+

“Perfect,” said Zoe.

+

Once they were out of earshot, Zoe leaned in. “She really doesn’t know, does she? About Sophie.”

+

We shook our heads.

+

“They’re nothing alike,” I said. “Different planets.”

+

“Sophie still avoiding you?” Zoe asked.

+

I nodded. “Mostly. I saw her once. She didn’t stop.”

+

Beth’s face darkened. “I can still see her face. That night. Just after she gave the order.”

+

Zoe blinked. “You think she did?”

+

Beth didn’t hesitate. “She knew what Rick was going to do. That club… there’s more to it. You think that, don’t you?”

+

Zoe nodded slowly. “Graham thinks so too. Says something stinks, but he hasn’t nailed it yet.”

+

Beth’s voice dropped to a whisper. “Mum wasn’t killed for nothing. Adam either. Rick even said so.”

+

“Has Graham got anything else out of Rick?” I asked.

+

“Nothing solid,” Zoe replied. “He’s clammed up. He’s gone ‘No comment’ on everything.”

+

“And our aunt? Still scared?” Beth asked.

+

“He’s working on her. She’s terrified. He’s got to go gently.”

+

Beth’s voice hardened. “What about Baker?”

+

Zoe’s face tightened. “He’s not in the frame. Yet. Graham’s gathering evidence, but if he pushes too soon, Baker will cover his tracks.”

+

Rebecca and Rosie returned, laughing.

+

“What have you two done now?” I asked.

+

“She dropped a tray of drinks on some poor guy,” Rosie grinned. “She’s worse than Rob.”

+

“Oi!” Rob protested. “That was once.”

+

“Twice,” I said, grinning, and leaned in to kiss him.

+

Beth rolled her eyes. “They’re going to be unbearable if they stay this happy.”

+

“Jealous?” I teased.

+

“A bit,” she admitted, then smiled. “But in the good way.”

+

The sun dipped lower, the sky glowing amber. For now, we had peace. But we hadn’t finished. Not yet. The flame had only just started to burn.

+ + +
+
+
+
diff --git a/CatherineLynwood/Views/Reckoning/Extras.cshtml b/CatherineLynwood/Views/Reckoning/Extras.cshtml new file mode 100644 index 0000000..5deb070 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Extras.cshtml @@ -0,0 +1,212 @@ +@{ + ViewData["Title"] = "Extras"; + + int? accessLevel = Context.Session.GetInt32("BookAccessLevel"); + int? accessBook = Context.Session.GetInt32("BookAccessMax"); +} + +
+
+ +
+
+ +
+

Your Exclusive Extras

+ + @if (accessBook == 1) + { +
+ @if (accessLevel >= 1) + { +
+
+
Epilogue
+

Immerse yourself in the Eplilogue to The Alpha Flame: Discovery. Join the team as they relax for a quite drink at the Barnt Green Inn

+ Read or Listen +
+
+ } + @if (accessLevel >= 2) + { +
+
+
Discovery Scrap Book
+

Take a look at my image scrapbook for The Alpha Flame: Discovery. View the images I used for inspiration when writing the various scenes within the book.

+ View Scrapbook +
+
+ +
+
+
Discovery Soundtrack
+

Have a listen to The Alpha Flame soundtrack. A selection of original songs written by me and put to music.

+ Soundtrack +
+
+ } + @if (accessLevel >= 3) + { +
+
+
Listen to The Alpha Flame: Discovery
+

Because you've purchased a premium physical copy of The Alpha Flame: Discovery, for a limited time this entitles you to listen to the audio version with no extra charge.

+ Listen to the Book +
+
+ +
+
+
Scrapbook: Maggie’s Designs
+

Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.

+ View Scrapbook +
+
+ } +
+ } + else if (accessBook == 2) + { + +
+ @if (accessLevel >= 1) + { + + } + else if (accessLevel >= 2) + { + + } + else if (accessLevel >= 3) + { + + } + else if(accessLevel >= 4) + { + + } +
+
+
Epilogue
+

Immerse yourself in the Eplilogue to The Alpha Flame: Discovery. Join the team as they relax for a quite drink at the Barnt Green Inn

+ Read or Listen +
+
+ +
+
+
Discovery Scrap Book
+

Take a look at my image scrapbook for The Alpha Flame: Discovery. View the images I used for inspiration when writing the various scenes within the book.

+ View Scrapbook +
+
+ +
+
+
Rubery Hill Photo Archive
+

Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.

+ Explore +
+
+ +
+
+
Scrapbook: Maggie’s Designs
+

Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.

+ View Scrapbook +
+
+ +
+ } + else if (accessBook == 3) + { +
+
+
+
Epilogue
+

Immerse yourself in the Eplilogue to The Alpha Flame: Discovery. Join the team as they relax for a quite drink at the Barnt Green Inn

+ Read or Listen +
+
+ +
+
+
Discovery Scrap Book
+

Take a look at my image scrapbook for The Alpha Flame: Discovery. View the images I used for inspiration when writing the various scenes within the book.

+ View Scrapbook +
+
+ +
+
+
Rubery Hill Photo Archive
+

Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.

+ Explore +
+
+ +
+
+
Scrapbook: Maggie’s Designs
+

Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.

+ View Scrapbook +
+
+ +
+ } + + + + +
+ +@section Meta{ + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/HowToBuy.cshtml b/CatherineLynwood/Views/Reckoning/HowToBuy.cshtml new file mode 100644 index 0000000..220a810 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/HowToBuy.cshtml @@ -0,0 +1,165 @@ +@{ + 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 Direct (Save & Support Author) + + + Buy on Amazon + +

ISBN 978-1-0682258-1-9

+

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

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

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

ISBN 978-1-0682258-0-2

+

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

Listen to the entire book for free on ElevenLabs (Elevenlabs subscription required):

+ + 🎧 Listen on ElevenLabs + +
+
+
+
+ + +@section Scripts{ + + + +} + diff --git a/CatherineLynwood/Views/Reckoning/Index.cshtml b/CatherineLynwood/Views/Reckoning/Index.cshtml new file mode 100644 index 0000000..7a34857 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Index.cshtml @@ -0,0 +1,285 @@ +@model CatherineLynwood.Models.TitlePageViewModel + +@{ + var releaseDate = new DateTime(2026, 4, 1); + ViewData["Title"] = $"The Alpha Flame: Reckoning — Pre-Release (out {releaseDate: d MMMM yyyy})"; +} + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+

The Alpha Flame: Reckoning

+

+ Book 2 of the Alpha Flame trilogy. + The danger didn’t end with Discovery. It got smarter. +

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

The Alpha Flame: Reckoning

+ +

+ Two flames, once divided, now burn as one. Bound by blood, scarred by secrets, they rise to face the fire that made them. +

+ +

+ After everything they survived in Discovery, Maggie and Beth should finally be free. + Instead, the nightmares follow them into daylight, and the past starts tugging at them with intent. + Beth is still rebuilding herself piece by piece. Maggie is trying to hold everything together. + And somewhere in the background, the people who benefitted from Beth’s silence are quietly noticing that she is no longer alone. +

+ +
+ +

The search for truth

+ +

+ The girls return to the fragments their mother left behind: the memory tin, the poem, and the wedding photograph. + What once felt like sentimental debris starts to look like a trail. A puzzle built on purpose. + Maggie, driven and sharp, refuses to accept that their mother’s life ended without meaning. + Beth, fragile but braver than she realises, wants answers even if they hurt. +

+ +

+ Their search pulls them toward Wales, toward names and places their mother never explained, and toward people who remember her + not as a ghost story, but as a living woman with fire in her bones. + The closer they get, the more they realise they are not the only ones searching. +

+ + + +
+ + + + + + +
+ Tip: Reckoning follows directly after Discovery. + If you’re new to the series, start there for maximum emotional damage. +
+ +
+
+
+ +
+
+ + +
+
+
+

The Alpha Flame: Reckoning

+

When the past returns, it doesn’t knock. It takes.

+
+ +
+ +

The watcher returns

+ +

+ Back in Birmingham, the threat stops lurking and starts moving. + A blue Ford Escort appears outside a safe doorstep and turns a tense conversation into a race for survival. + The girls flee through back roads and service lanes, only to be hunted in the open, the Escort glued to their rear bumper like fate. + It is not random. It is not a coincidence. Someone has been sent. +

+ +

+ Worse still, the fallout reaches the police. Mrs Patterson, the neighbour who knows too much, turns up dead. + Graham arrives with questions he does not want to ask, and DI Baker arrives with the kind of presence that poisons a room. + The message is clear: even authority cannot be assumed safe. +

+ +
+ +

Wales, family, and the key

+ +

+ Wales offers a temporary illusion of peace: wide skies, old farms, warm kitchens, and people who speak your mother’s name + like she mattered. For the first time, Beth and Maggie experience something dangerously close to ordinary. + But the ordinary does not last. +

+ +

+ The poem changes. Literally. A second version appears, with new stanzas that reframe everything. + What the girls believed was a single trail is revealed as a deliberate split: one path for the wrong eyes, + another for the right ones. Their mother planned for predators. She built decoys. + And she hid the real direction in plain sight, waiting for the day her daughters would be ready to understand it. +

+ +

+ A deposit box key is found, tucked away where it was never supposed to be noticed. + Now the question is not whether their mother left something behind, but what she was protecting, and from whom. +

+ +
+ +

Names that change everything

+ +

+ Answers arrive in the worst way: through memory, confession, and people who were there when the damage began. + Gareth finally tells the story of Annie, Elen, and the night the trap was set. + He names the man at the centre of it all: Simon Jones. + Not a faceless monster, but a real figure with reach, money, and a network built on fear. +

+ +

+ The revelation detonates Beth’s fragile stability. The past is not behind her. + It is connected to everything still happening now, and to Sophie’s world in Birmingham. + The girls are not just searching for identity anymore. + They are standing on a fault line that runs straight through power, crime, and corruption. +

+ +
+ +

The boxes open

+ +

+ When the second deposit box finally opens, it is not comfort inside. It is evidence. + Their mother left them a ledger of real accounts, receipts, photos, recordings, and a copied key tied to Sophie’s safe. + It is a package designed to destroy Simon Jones, but it comes with a brutal catch: + the final identities, the protected names, the ones behind the coded initials, are stored elsewhere, + locked in Sophie’s personal safe in a red case. +

+ +

+ Their mother did not leave them a neat answer. + She left them a weapon, and a warning. +

+ +
+ +

The reckoning hits home

+ +

+ As the net tightens, the violence stops being distant. It becomes intimate. + During a final clash, Rob steps into the line of fire for Maggie. + In the silence of a ruined place, he bleeds out in her arms, and the future they were building collapses in seconds. + His death is not just loss. It is consequence. + It is the cost of standing beside someone the darkness wants back. +

+ +

+ Grief does not pause the danger. If anything, it sharpens it. + Maggie and Beth are no longer just survivors. + They are holding proof that could bring down an empire, and every powerful person connected to it. +

+ +
+ +

The knock

+ +

+ The story closes with the world catching up. + The authorities circle. Baker’s shadow stretches. + And just when it seems the girls might get a breath, duty arrives on the doorstep. + The past has claimed another body, and someone needs a suspect. + The final beat lands exactly as it should: + a knock that changes everything, and the unmistakable sense that what comes next will not be gentle. +

+ + + + +
+
+
+ + +@*
+
+
+

Preview content

+

+ Previews, sample chapters, and reader reviews will appear here once they’re ready. + Until then, Discovery is available now. +

+ +
+
+
*@ + +@section Scripts { + + +} + +@section Meta { + +} diff --git a/CatherineLynwood/Views/Reckoning/Listen.cshtml b/CatherineLynwood/Views/Reckoning/Listen.cshtml new file mode 100644 index 0000000..0037dae --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Listen.cshtml @@ -0,0 +1,674 @@ +@{ + ViewData["Title"] = "Listen to The Alpha Flame: Discovery"; +} + +
+
+ +
+
+ +
+
+
+

Loading...

+
    + +
    + +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + +@section Scripts { + +} diff --git a/CatherineLynwood/Views/Reckoning/MaggiesDesigns.cshtml b/CatherineLynwood/Views/Reckoning/MaggiesDesigns.cshtml new file mode 100644 index 0000000..b382be4 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/MaggiesDesigns.cshtml @@ -0,0 +1,375 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Maggie's Designs"; +} +
    +
    + +
    +
    + +
    +

    Maggie's Designs

    +

    + I love fashion and I trued to weave that into Maggie's charactger. Helped by AI I really enjoyed coming up with outfits + for Maggie to wear in the various scenes of the book. Lots were so far out there that they didn't make it to the final edit, + or the descriptions were so detailed that you've had nodded off. +

    +

    + That said, no matter what the outfit was I found working from a visual prompt enable me to write more vividly about it. +

    + + +
    +
    + +
    + “She didn’t dress to impress. She dressed to become, a force wrapped in colour, stitched in self-belief.” +
    + +
    +
    +
    “The Elle Dress” – Fully Formed
    + +

    This is it, the dress that stopped hearts and turned pages. The one Maggie wore for *Elle*, unapologetic and unforgettable. There’s nothing shy about this piece. It’s all precision and provocation; a sculpted plunge, a defiant cut-out, a hemline that dances between lingerie and high fashion. Every detail speaks Maggie’s language, fire without fuss, beauty without permission. She didn’t just wear this dress… she claimed it. And in doing so, she claimed herself.

    + +

    The deep teal shimmered like danger in low light, somewhere between mermaid and menace. I remember writing the scene and thinking, *this isn’t about being sexy, it’s about being seen*. This dress is armour. Soft in texture, sharp in purpose. The fishnet tights? That was Maggie’s touch. A reminder that no matter how glossy the set, she brought her own edge with her. Always.

    + +

    There may be another version of this dress later, refined, reimagined. But this one? This is the original. The raw truth. The moment the world turned to look… and Maggie didn’t flinch.

    + +
    +
    + + +
    +
    + +
    + “She didn’t rise from ashes. She bloomed from the soil, raw, rooted, and wild beyond taming.” +
    + +
    +
    +
    “Forest Siren” – The Evolution of Elle
    + +

    This is what happens when a woman finds her voice, when the flame that once flickered is now fully ablaze, controlled, focused, and utterly arresting. This dress is the next chapter after the *Elle* shoot, not just an outfit, but a statement. A transformation. A whispered prophecy realised. The original was power through seduction… but this? This is power through presence.

    + +

    I imagined Maggie walking barefoot across stone in this. Not for a crowd. Not even for the camera. Just for herself. The soft, trailing green, almost moss-like in motion, conjures wild things and whispered rebellions. The floral lace still clings, but it no longer begs to be noticed. It simply exists, unafraid, untamed. The high slit and fishnets remain, a signature. A nod to the girl who fought to survive. But the gown? That belongs to the woman she’s become.

    + +

    There’s a reason this version was never photographed for the magazine. It wasn’t fashion anymore. It was folklore. A garment you glimpse once and never forget. And like Maggie, it doesn’t ask you to understand it. It only asks that you remember.

    + +
    +
    + + +
    +
    + +
    + “She didn’t wear it to be wanted. She wore it to be undeniable.” +
    + +
    +
    +
    “Ivory Fire” – The One That Smouldered Off-Page
    + +

    This dress never appeared in the book, but it haunted the edges of it. I imagined Maggie in it once, just once, and the image never left. It was lace, yes, but not gentle. The kind of lace that clung like memory and cut like truth. It wasn’t designed to cover; it was designed to challenge. Petals traced over her chest like secrets too dangerous to speak aloud, their edges dipping into skin with a kind of deliberate precision that made it impossible to ignore her.

    + +

    She didn’t wear this to seduce. She wore it to reclaim. I pictured her standing still in it while the world moved around her, daring it to catch up. The neckline plunged, but it was her gaze that undressed the room. In the end, it was too much for the scene I was writing, too sharp, too strong, too unforgettable. So I left it behind. But like so much of Maggie, it lived on in the shadows between pages. Not forgotten. Just waiting.

    + +
    +
    + + +
    +
    + +
    + “It burned beneath the fabric, not just colour, but something molten and unspoken. She was the match and the flame.” +
    + +
    +
    +
    “Ember Veins” – The Prototype
    + +

    This was the dress that nearly made it, an early vision of what would eventually evolve into Maggie’s infamous jumpsuit in Chapter 7. I called it *Ember Veins* because the pattern reminded me of fire trapped under silk, alive just beneath the surface. The fabric moves like it’s breathing. Every flick of orange, red, and teal threads through the design like molten emotion stitched into form. It’s bold, unapologetic… and yet, it never quite felt like the right fit for the scene. Too regal. Too statuesque. Not enough of Maggie’s street-born swagger.

    + +

    But the essence is here. The deep plunge that dares the room to judge her. The sculpted waist that declares power, not permission. And the colours, oh, the colours. They felt like Maggie’s moods during that chapter: blazing, unpredictable, and impossible to ignore. This wasn’t something she would wear to blend in; it was what she’d wear to set a room on fire.

    + +

    Ultimately, the jumpsuit won, it had grit, mobility, and a whisper of rebellion. But *Ember Veins* still has a place in Maggie’s story. Not on the page, perhaps… but in the margins, smouldering quietly, waiting to be remembered.

    + +
    +
    + + +
    +
    + +
    + “It wasn’t about seduction. It was about control, about saying, ‘You can look… but you don’t get to touch.’” +
    + +
    +
    +
    “Midnight Teal” – The Dress Before Elle
    + +

    Before Maggie stunned the world in the pages of *Elle*, there was this. A raw, electric concept that pulsed with possibility. I called it *Midnight Teal*, a piece that sat somewhere between lingerie and defiance, stitched not for comfort but for confrontation. This was Maggie untamed, unfiltered, unapologetically herself. It wasn’t designed for the high street or a Paris runway… it was born for shadows and stares, for flickering candlelight and whispered thoughts.

    + +

    The lace, tangled like secrets, reveals more than it hides. The fishnets ground her in the kind of grit only Maggie could carry. And that choker? A black ribbon that says “you can look, but you don’t get to own.” I always imagined her wearing this not to seduce, but to reclaim. Not to tease, but to dare. This wasn’t about being pretty. It was about being powerful.

    + +

    Ultimately, it was too much for the magazine shoot. Elle needed elegance. This was rebellion. But I keep it here, because it mattered. Because somewhere in Maggie’s soul, this dress still lives, wild, sensual, fearless. A dress not worn for an audience, but for herself.

    + +
    +
    + + +
    +
    + +
    + “I can be soft. But don’t mistake me for breakable.” +
    + +
    +
    +
    “Sleek Intentions” – The Outfit That Almost Was
    + +

    Before the white two-piece took its place in Chapter 15, this was the look. A vision in midnight black, bold, sculpted, unforgiving. I imagined Maggie in this as a weapon disguised as elegance. The high neck and sheer sleeves gave it structure, control… but the body-hugging lines spoke of something else entirely. Power. Restraint. And maybe a little hunger too. She wasn’t dressing for flirtation; she was dressing for impact.

    + +

    This was meant to be her first appearance at Ricardo’s, a table full of family, wine, and a quiet undercurrent of testing the waters. But the outfit changed because the tone did. White softened the edges. A two-piece gave her room to move, to breathe, to step into that moment with grace rather than dominance. And yet I still love this version. It shows the side of Maggie that doesn’t compromise, the girl who grew up armoured in silence and attitude.

    + +

    *Sleek Intentions* never made it to the page, but it belonged to the story all the same. The decision not to wear it says as much as if she had. Sometimes, power is in the pivot, in choosing softness when the world expects sharp.

    + +
    +
    + + +
    +
    + +
    + “She didn’t escape the fire. She walked through it, and the flowers grew behind her.” +
    + +
    +
    +
    “Where the Poppies Burn” – A Dream of Freedom
    + +

    This one was never meant for the story itself, not directly. It was more of a whisper behind the writing. A vision I carried with me in quiet moments: Maggie, walking barefoot through a field of fire-tipped poppies, the world golden and glowing around her. She’s not looking back. She doesn’t need to. Whatever held her is gone. Whatever comes next is hers to decide.

    + +

    The dress is barely there, a gauze of lace and suggestion, soft as breath, flowing like memory. It’s not about seduction, not here. It’s about shedding. About choosing vulnerability in a world that demanded armour. Her hair is wild, her steps silent, and the light clings to her like it knows she’s survived something most never would.

    + +

    I once considered using this for the cover. It would have been unconventional, maybe too symbolic, but it captured a truth. Not about what Maggie wears, or where she walks, but who she is when no one is watching. This image was never part of the story on the page… but it’s part of the soul underneath it.

    + +
    +
    + + +
    +
    + +
    + “You don’t rise from fire without learning how to burn.” +
    + +
    +
    +
    “Verdant Fury” – The Goddess Unleashed
    + +

    This was never meant to be subtle. This was me asking, *what if Maggie didn’t just survive… what if she ruled?* What if all the hurt, all the hunger, all the fire she'd kept bottled up was no longer something she hid, but something she wore? The result was this: *Verdant Fury*. A vision in deep emerald and gold, clinging to her like ivy laced with flame.

    + +

    There’s a mythic quality to this look, part forest queen, part fallen angel, all defiance. The sculpted bodice doesn’t just highlight her form, it *armours* it. The gloves, shredded and clinging, feel like echoes of a battle already won. She stands here not as a girl escaping the past, but as a woman who’s scorched the path behind her. And those eyes, they’re not asking for permission. They’re issuing a challenge.

    + +

    This outfit never had a scene. It was too much to contain. Too electric, too dangerous. But it lives in the spirit of Maggie all the same. In the moments where she turns, chin lifted, and dares the world to tell her she can’t. It’s not fashion. It’s a reckoning draped in green.

    + +
    +
    + + +
    +
    + +
    + “She wore it like a dare, and Zoe answered with a kiss.” +
    + +
    +
    +
    “Unveiling Desires” – The Painted Flame Jumpsuit
    + +

    This was never a maybe. This jumpsuit was destined to be worn. From the moment I imagined Maggie walking into that nightclub on New Year’s Eve, I saw her in this, a riot of colour, molten silk clinging to her with intent. Gold, vermilion, violet, turquoise… each hue brushed like fire across fabric, alive with movement, bold without apology. She didn’t just wear it, she ignited it.

    + +

    Everything about it was deliberate. The plunging neckline, the fitted waist, the shimmer that caught the light every time she turned, it was a siren song, yes, but also a declaration. She made it herself, of course. Maggie’s talent always came from that raw place inside her, where fire met finesse. And in this look, her artistry wasn’t just visible, it was undeniable.

    + +

    The jumpsuit became more than fashion. It was foreplay. Power. The spark that lit the fuse between her and Zoe, making their chemistry explosive and immediate. This outfit had presence. It was the flame before the kiss, the stroke before the sigh. And it belongs in this scrapbook because it didn’t just make it into the book, it helped define it.

    + +
    +
    + + +
    +
    + +
    + “She didn’t wear it… but for a moment, she almost did.” +
    + +
    +
    +
    “Whispers in Lace” – The Dress That Was Too Much
    + +

    This gown was a daydream. An experiment in elegance. I imagined Maggie twirling through Ricardo’s grand hallway, all lace and light, every step sweeping the floor like she was born to haunt ballrooms. But as soon as I saw it fully realised, I knew, this wasn’t her moment for that. This dress belonged to a different story. One where Maggie danced, yes, but not in a world as grounded as hers. It was too refined, too ethereal, too… not quite right.

    + +

    Still, there’s something about it that I loved. The off-shoulder cut, the illusion of fragility, the way it billowed with every motion. It captured the romantic part of Maggie that often hides behind her fire, the dreamer, the artist, the girl who still, despite everything, believes in beauty. But I needed her to walk into Ricardo’s with strength, not softness. Confidence, not fantasy. This was grace when what I needed was poise with a bit of punch.

    + +

    So the dress stayed in the wings. It never made it onto the page. But like many of Maggie’s almost-moments, it still deserves to be seen. Because sometimes, even the wrong outfit tells us something honest about who she is underneath it all.

    + +
    +
    + + +
    +
    + +
    + “She wore hope in the shape of a dress. And for a moment, she almost believed it would be enough.” +
    + +
    +
    +
    “Lace & Lager” – The First Date Dress
    + +

    This was the one. The dress Maggie wore the night she finally let herself hope for something real, and got two pints of beer tipped over her for the trouble. I designed it around contradiction: soft lace sleeves clinging like whispered promises, paired with a defiant red skirt that billowed like a dare. It wasn’t subtle. It wasn’t supposed to be. It was Maggie stepping into the world not as someone surviving, but as someone choosing to be seen.

    + +

    The top was reworked from a more elegant concept, too much for Ricardo’s, but perfect once grounded by the rough texture and boldness of that crumpled scarlet skirt. I loved that about her. How she blended grace with grit. The result was vulnerable and fearless all at once, which is probably why the moment hurt so much when it all went wrong.

    + +

    She looked stunning that night. I remember writing that scene with a tightness in my chest, knowing exactly how much that outfit meant to her, not just as a creation, but as a risk. And seeing it ruined, soaked in lager and shame, broke my heart. But that was the point. This dress, like Maggie, deserved better. And eventually… she gets it.

    + +
    +
    + + +
    +
    + +
    + “It made her feel powerful… but not like herself.” +
    + +
    +
    +
    “Velvet Resolve” – The One That Stayed Behind
    + +

    This was a contender, a serious one. There was a moment when I imagined Maggie standing at the top of the stairs in this, every line of the dress perfectly composed, every eye in the restaurant turning to look. The plunging black velvet bodice, the way it folded into that crimson skirt… it was elegance incarnate. Mature. Commanding. And, in the end, just a little too much.

    + +

    Because Maggie, for all her power, wasn’t trying to impress that night, not with poise. She wanted to feel beautiful, yes, but she also wanted to feel *real*. To be herself, raw edges and all. This dress was breathtaking, but it was armour. And what she needed then was something that breathed with her, not something that held her still.

    + +

    But I still keep it here, in the scrapbook. Because this was the version of Maggie who might have walked into that meal pretending she wasn’t scared. The one who hid every bruise behind glamour. It didn’t make it to the page, but it came very close.

    + +
    +
    + + +
    +
    + +
    + “She didn’t need a scene to wear this. Just a window, and a reason to breathe.” +
    + +
    +
    +
    “Threadbare Ghosts” – The Dress Without a Scene
    + +

    This one never had a chapter. No nightclub. No first date. No dramatic spill or kiss behind a curtain. It wasn’t made for anything, and maybe that’s why I love it. Because sometimes, Maggie just exists… not as a character in motion, but as a feeling. A breath. A girl wrapped in sunlight and silence, not trying to fight or impress or survive, just being. And this is what that moment looked like in my head.

    + +

    The bodice is intricate, almost antique, like something stolen from a forgotten theatre. Lace, delicate and curling like memory. Faint threads of rust red and soft bone hues bleeding into the fabric, as if it once knew passion and never quite let go. She looks like something out of time. Something haunting and utterly alive. And that’s the magic of it, she doesn’t need to move to hold you. She just needs to look up, like this, and you’re caught.

    + +

    I never found the right place to write this dress in. But I never let it go, either. It stayed on my desk, on a scrap of paper, next to notes that never became dialogue. Because some images don’t need stories. They *are* the story… just quietly, beautifully, waiting.

    + +
    +
    + + +
    +
    + +
    + “She wore it first. Beth made it hers. But what stitched them together was never fabric.” +
    +
    +
    +
    “Summer Soft” – The Outfit They Shared
    + +

    This outfit was never meant to be a showstopper. It wasn’t fire or lace or velvet. It was light, deliberately so. The white cotton skirt, simple and sun-washed, the top just delicate enough to feel like a whisper. This was the first outfit Maggie wore to Ricardo’s, when she needed to feel both presentable and herself. It wasn’t designed to turn heads… and yet it did, quietly, effortlessly. She wore it with that rare kind of grace that doesn’t try, and so becomes unforgettable.

    + +

    Later, she lent it to Beth, and that’s when it truly earned its place in the story. Because clothes carry energy. They hold memory. And in that moment, Maggie wasn’t just lending an outfit. She was offering safety, trust, sisterhood before either of them even knew the word for it. Beth, wrapped in Maggie’s confidence, stepping into her own space, her own choices, it made me cry when I wrote it. Still does.

    + +

    It’s not the most elaborate design in the scrapbook, not by a long shot. But maybe that’s what makes it so important. Because sometimes, what matters most isn’t how it looks… but who it becomes part of.

    + +
    +
    + +
    + + +@section Meta { + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/Reviews.cshtml b/CatherineLynwood/Views/Reckoning/Reviews.cshtml new file mode 100644 index 0000000..a5fdb99 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Reviews.cshtml @@ -0,0 +1,101 @@ +@model CatherineLynwood.Models.Reviews +@{ + ViewData["Title"] = "Reader Reviews – The Alpha Flame: Discovery"; +} + +
    +
    +
    + +
    +
    +
    +
    +
    +

    Reader Reviews

    +

    Here’s what readers are saying about The Alpha Flame: Discovery. If you’ve read the book, we’d love for you to share your thoughts too.

    +
    + +
    + @if (Model?.Items?.Any() == true) + { + foreach (var review in Model.Items) + { + var fullStars = (int)Math.Floor(review.RatingValue); + var hasHalfStar = review.RatingValue - fullStars >= 0.5; + var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0); + var reviewDate = review.DatePublished.ToString("d MMMM yyyy"); + +
    +
    +
    + + @for (int i = 0; i < fullStars; i++) + { + + } + @if (hasHalfStar) + { + + } + @for (int i = 0; i < emptyStars; i++) + { + + } + + @Html.Raw(review.ReviewBody) +
    + @review.AuthorName on + + @if (string.IsNullOrEmpty(review.URL)) + { + @review.SiteName + } + else + { + @review.SiteName + } + @reviewDate +
    +
    +
    +
    + + } + } + else + { +

    There are no reviews to display yet. Be the first to leave one!

    + } +
    +
    +
    +
    + +@section Meta{ + + + + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/ScrapBook.cshtml b/CatherineLynwood/Views/Reckoning/ScrapBook.cshtml new file mode 100644 index 0000000..4adbd7a --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/ScrapBook.cshtml @@ -0,0 +1,224 @@ +@{ + ViewData["Title"] = "The Alpha Flame: Discovery Scrap Book"; +} +
    +
    + +
    +
    + +
    +

    Discover Scrapbook

    +

    + On this page I've included some of the images that helped inspire me to write The Alpha Flame: Discovery. +

    +

    + Some of them are fictional, generated by AI, others are real, both recent and archive photos. +

    +

    + I find that when I'm writing I like to imagine the scene. So having a visual guide helps me tremendously. + So I might, for example, ask AI to generate an image of a luxurious restaraunt. The I would look at the result + and tweak it, until what I was looking at matches what was in my imagination. I also find it useful to do the reverse. + I might write a description of a particular location, Beth's flat was one of them, and then feed that into AI to see + if it generated what I'd described. This was really useful because it helped me work out whether there was enough detail. + My thoughts were that if AI can draw what I've described, then I'm sure you as a reader will be able to imagine something + similar. Hopefully it's worked. +

    + + +
    +
    + +
    +
    +
    The Rubery Flyover
    +

    + A number of scenes in The Alpha Flame: Discovery are set, or at least start here. The Rubery flyover carries the A38 Bristol Road + down to meet the M5. It's the main road in and out of the city from the South West. Quite why the floyover exists is something of + a mystery to me. Apart from allowing traffic to pass underneath it, past "Beth's Bench", it seems to serve no purpose. It doesn't + bridge any sort of gap in the landscape. Neither does it provide access to a higher ground further along. +

    +

    + Many years ago I used to live in this area and travelling underneath the flyover really gave me the creeps. As I describe in the book + there were often many unsavoury characters seated on the benches. And thre genuinely were women offering their bodies to whoever + happened to drive past. +

    +
    +
    + + +
    +
    + +
    +
    +
    Beth and Her Flat
    +

    + In the beginning Beth's flat was even more depressing that it is described in the book. The idea of a sterotypical + early 1980s council flat was too hard to resist. However once I had developed her personality a little further it + became obvious that she would have a lot more pride in where she lived, even if she had no money. +

    +

    + In reality the block of flats that I put Beth in are only two storeys on top of a row of shops. But for the + purpose of the book I made them into one of the taller blocks that can be found all over the area. +

    +
    +
    + + +
    +
    + +
    +
    +
    Rubery Hill Hospital
    +

    + Now this place used to genuinely give me the creeps. It was built way back in the 1800s and as I say in the book, + was origonally known as "The City of Birmingham Lunatic Asylum". It was renamed many times over it's long life + but always had links to what we would call "mental health" these days. From what I have found out the only deviation + from this was during the war, when it was a hospital for injured soldiers. +

    +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    Maggie in Her Jumpsuit
    +

    + So, the scene where Maggie goes to Limelight and walks across the dance floor to go and say hello to Zoe and Graham. Is this what you imagined? + I doubt it, but it was what was in my mind. The original description of this outfit went on for over a page, and I think you can see why. + Unfortuantely it got severaly cut in the edit. You're probably quite thankful for that! +

    +

    + But wouldn't you just love to wear it! +

    +
    +
    + + +
    +
    + +
    +
    +
    Ricardo's
    +

    + The extremely lavish Riscrod's restaraunt went through many changes, and evloved into something more luxurious every + single time. I'm not sure this image does it justice, but I hope by my descriptions you got the feeling of the sheer + opulance within it. +

    +

    + In reality Northfield never had any such restaraunt, it's simply not that sort of place. But it's nice to think that + it could have. +

    +
    +
    + + +
    +
    + +
    +
    +
    Beth and Her Flat
    +

    + In the beginning Beth's flat was even more depressing that it is described in the book. The idea of a sterotypical + early 1980s council flat was too hard to resist. However once I had developed her personality a little further it + became obvious that she would have a lot more pride in where she lived, even if she had no money. +

    +

    + In reality the block of flats that I put Beth in are only two storeys on top of a row of shops. But for the + purpose of the book I made them into one of the taller blocks that can be found all over the area. +

    +
    +
    + + +
    +
    + +
    +
    +
    Rubery Hill Hospital
    +

    + Now this place used to genuinely give me the creeps. It was built way back in the 1800s and as I say in the book, + was origonally known as "The City of Birmingham Lunatic Asylum". It was renamed many times over it's long life + but always had links to what we would call "mental health" these days. From what I have found out the only deviation + from this was during the war, when it was a hospital for injured soldiers. +

    +
    +
    + +
    + + +@section Meta { + +} \ No newline at end of file diff --git a/CatherineLynwood/Views/Reckoning/Soundtrack.cshtml b/CatherineLynwood/Views/Reckoning/Soundtrack.cshtml new file mode 100644 index 0000000..6b12c5c --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Soundtrack.cshtml @@ -0,0 +1,198 @@ +@model List +@{ + ViewData["Title"] = "Alpha Flame • Soundtrack"; +} + +
    +
    +

    The Alpha Flame • Soundtrack

    +

    Eight original tracks inspired by key chapters; listen while you read…

    +
    + +
    + @if (Model != null && Model.Any()) + { + var index = 0; + foreach (var track in Model) + { + var id = $"track-{index++}"; +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    +

    @track.Title

    + @if (!string.IsNullOrWhiteSpace(track.Chapter) || !string.IsNullOrWhiteSpace(track.Description)) + { +

    + @if (!string.IsNullOrWhiteSpace(track.Chapter)) + { + Chapter: @track.Chapter + } + @if (!string.IsNullOrWhiteSpace(track.Chapter) && !string.IsNullOrWhiteSpace(track.Description)) + { + + } + @if (!string.IsNullOrWhiteSpace(track.Description)) + { + @track.Description + } +

    + } + +
    + @if (!string.IsNullOrWhiteSpace(track.LyricsHtml)) + { + @Html.Raw(track.LyricsHtml) + } +
    + +
    + + +
    + + + +
    +
    +
    +
    +
    + } + } + else + { +
    +
    + Tracks will appear here soon. +
    +
    + } +
    + + +
    + + + +@section Scripts { + +} diff --git a/CatherineLynwood/Views/Reckoning/Trailer.cshtml b/CatherineLynwood/Views/Reckoning/Trailer.cshtml new file mode 100644 index 0000000..2bfac49 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/Trailer.cshtml @@ -0,0 +1,488 @@ +@model CatherineLynwood.Models.FlagSupportViewModel + +@{ + ViewData["Title"] = "The Alpha Flame - Coming Soon"; +} + + +
    +
    +
    + +
    +

    The Alpha Flame: Discovery

    +

    A gritty Birmingham crime novel set in 1983

    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + +@if (DateTime.Now < new DateTime(2025, 8, 21)) +{ +
    +
    +
    +

    Released in:

    +
    + 0d + 00h + 00m + 00s +
    + + + +
    +
    +
    +} + + + +
    +
    +
    +

    Please show your support

    +

    Tap your flag to show your support.

    +
    +
    +
    + @foreach (var kv in Model.FlagCounts) + { + string pulse = ""; + 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() + }; + + if (kv.Selected) + { + pulse = "btn-pulse"; + } + + } +
    + + + +
    +
    +
    + +
    +
    +
    +

    Listen to The Alpha Flame theme tune
    The Flame We Found

    + +
    +
    +
    + + + +
    +
    +
    +

    A glimpse inside

    +
    + +
    +
    +
    +
    +
    +

    + The Alpha Flame: Discovery +

    +

    + Some girls survive. Others set the world on fire. +

    +

    + She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away. +

    +

    + But nothing prepares her for Beth. +

    +

    + As she digs deeper into Beth’s world, Maggie finds herself pulled into the shadows, a seedy underworld of secrets, survival, and control, where loyalty is rare and nothing is guaranteed. The more she uncovers, the more she realises this isn’t someone else’s nightmare. It’s her own. +

    +

    + The Alpha Flame: Discovery is a gritty, emotionally charged thriller that pulls no punches. Raw, real, and anything but a fairy tale, it’s a story of survival, sisterhood, and fire. +

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

    + I eased the TR6 down a side street, the headlights sweeping over a figure shifting in the shadows. A movement to my left. A woman, young, her face pale beneath the heavy makeup, stepped forward as I slowed at the junction. She leaned down to my passenger window, so close I could see the faint smudge of lipstick at the corner of her mouth. +

    +

    + A loud knock on the glass made me jump. +

    +

    + “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. +

    +

    + My stomach tightened. +

    +

    + I wasn’t looking for anything. Not really. But I didn’t drive away either. +

    +

    + She was close now, close enough that I could see the dark liner smudged beneath her eyes, the glint of something unreadable in her gaze. Not quite curiosity. Not quite suspicion. Just a quiet knowing. +

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

    + “Maggie… wait.” +

    +

    + She turned as I crouched down. My stomach dropped. +

    +

    + It was a sweatshirt. Pink. Faded. Cartoon print on the front, cracked with age and wear. Garfield, grinning. +

    +

    + I reached out slowly, fingertips brushing the fabric. The left sleeve was soaked, stiff with something dark. +

    +

    + Blood. +

    +

    + “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.” +

    +

    + Maggie dropped to her knees beside me, torch trembling in her grip. “Bloody hell. You’re right.” +

    +

    + For a second neither of us moved. The building suddenly felt tighter, like it was watching us. +

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

    + She turned in the water, soaked to the waist, flinging droplets everywhere. +

    +

    + “Maggie! Come on!” she shouted, laughing. “You’ve got to feel this!” +

    +

    + I didn’t hesitate. +

    +

    + I peeled off my hoody and shorts, left them in a heap on the rocks, and sprinted after her, my bikini clinging tight to my skin in the salty breeze. The sand stung slightly as I ran, then came the cold slap of the sea, wrapping around my legs and dragging a breathless laugh out of me. +

    +

    + Beth was already dancing through the waves like a lunatic. +

    +

    + We collided mid-splash, both of us soaked, screaming and laughing like we were eight years old again, like we’d somehow got all those childhood summers back in one moment. + The sea was freezing, but we didn’t care. +

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

    Coming 21st August 2025 to major retailers.

    + +
    +
    +
    + + + +@* + + + + *@ +@section Scripts { + + + + + + +@if (DateTime.Now < new DateTime(2025, 8, 21)) + { + + } + + +} diff --git a/CatherineLynwood/Views/Reckoning/_Layout.cshtml b/CatherineLynwood/Views/Reckoning/_Layout.cshtml new file mode 100644 index 0000000..a319657 --- /dev/null +++ b/CatherineLynwood/Views/Reckoning/_Layout.cshtml @@ -0,0 +1,29 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} + +@section Meta { + @RenderSection("Meta", required: false) +} + +@section BackgroundVideo { +
    +
    + +
    +
    +
    +} + +@RenderBody() + +@section Scripts { + @RenderSection("Scripts", required: false) +} diff --git a/CatherineLynwood/Views/Shared/Components/BuyPanel/Default.cshtml b/CatherineLynwood/Views/Shared/Components/BuyPanel/Default.cshtml new file mode 100644 index 0000000..77027a3 --- /dev/null +++ b/CatherineLynwood/Views/Shared/Components/BuyPanel/Default.cshtml @@ -0,0 +1,59 @@ +@model CatherineLynwood.Models.BuyPanelViewModel + +
    + +
    +

    Buy the Book

    + + + Best options for @Model.ISO2 + +
    + + @foreach (var group in Model.Groups.OrderBy(x => x.DisplayOrder) ) + { +
    +
    +
    + @if (string.IsNullOrWhiteSpace(group.Message)) + { +
    @group.GroupName
    + } + else + { +
    +
    + @group.GroupName: + @Html.Raw(group.Message) +
    +
    + } +
    +
    +
    + @foreach (var link in group.Links.OrderBy(x => x.Text)) + { + + } +
    +
    + } +
    diff --git a/CatherineLynwood/Views/Shared/_Layout.cshtml b/CatherineLynwood/Views/Shared/_Layout.cshtml index 34dd0e8..22a1152 100644 --- a/CatherineLynwood/Views/Shared/_Layout.cshtml +++ b/CatherineLynwood/Views/Shared/_Layout.cshtml @@ -11,6 +11,7 @@ + @RenderSection("CSS", required: false)