Save
This commit is contained in:
parent
8215d83f63
commit
fb4e139d9f
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
@ -154,18 +154,18 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
<PackageReference Include="Microsoft.Web.Administration" Version="11.1.0" />
|
||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="WebMarkupMin.AspNetCoreLatest" Version="2.19.0" />
|
||||
<PackageReference Include="WebMarkupMin.AspNetCoreLatest" Version="2.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
27
CatherineLynwood/Components/BuyPanel.cs
Normal file
27
CatherineLynwood/Components/BuyPanel.cs
Normal file
@ -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<IViewComponentResult> InvokeAsync(string iso2, string src, string title)
|
||||
{
|
||||
BuyPanelViewModel buyPanelViewModel = await _dataAccess.GetBuyPanelViewModel(iso2, title);
|
||||
buyPanelViewModel.Src = src;
|
||||
|
||||
return View(buyPanelViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<string, BuyLink> 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<IActionResult> 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<IActionResult> 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();
|
||||
}
|
||||
|
||||
19
CatherineLynwood/Controllers/CollaborationsController.cs
Normal file
19
CatherineLynwood/Controllers/CollaborationsController.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<IActionResult> 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";
|
||||
|
||||
12
CatherineLynwood/Controllers/IndieAuthorController.cs
Normal file
12
CatherineLynwood/Controllers/IndieAuthorController.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CatherineLynwood.Controllers
|
||||
{
|
||||
public class IndieAuthorController : Controller
|
||||
{
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
399
CatherineLynwood/Controllers/ReckoningController.cs
Normal file
399
CatherineLynwood/Controllers/ReckoningController.cs
Normal file
@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> Soundtrack()
|
||||
{
|
||||
List<SoundtrackTrackModel> 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<string, object>
|
||||
{
|
||||
["@context"] = "https://schema.org",
|
||||
["@type"] = "Book",
|
||||
["name"] = "The Alpha Flame: Discovery",
|
||||
["alternateName"] = "The Alpha Flame Book 1",
|
||||
["image"] = imageUrl,
|
||||
["author"] = new Dictionary<string, object>
|
||||
{
|
||||
["@type"] = "Person",
|
||||
["name"] = "Catherine Lynwood",
|
||||
["url"] = "https://www.catherinelynwood.com"
|
||||
},
|
||||
["publisher"] = new Dictionary<string, object>
|
||||
{
|
||||
["@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<Dictionary<string, object>>();
|
||||
double total = 0;
|
||||
foreach (var review in reviews.Items) total += review.RatingValue;
|
||||
|
||||
foreach (var review in reviews.Items.Take(take))
|
||||
{
|
||||
reviewObjects.Add(new Dictionary<string, object>
|
||||
{
|
||||
["@type"] = "Review",
|
||||
["author"] = new Dictionary<string, object>
|
||||
{
|
||||
["@type"] = "Person",
|
||||
["name"] = review.AuthorName
|
||||
},
|
||||
["datePublished"] = review.DatePublished.ToString("yyyy-MM-dd"),
|
||||
["reviewBody"] = StripHtml(review.ReviewBody),
|
||||
["reviewRating"] = new Dictionary<string, object>
|
||||
{
|
||||
["@type"] = "Rating",
|
||||
["ratingValue"] = review.RatingValue,
|
||||
["bestRating"] = "5"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
schema["review"] = reviewObjects;
|
||||
schema["aggregateRating"] = new Dictionary<string, object>
|
||||
{
|
||||
["@type"] = "AggregateRating",
|
||||
["ratingValue"] = (total / reviews.Items.Count).ToString("0.0", CultureInfo.InvariantCulture),
|
||||
["reviewCount"] = reviews.Items.Count
|
||||
};
|
||||
}
|
||||
|
||||
schema["workExample"] = new List<Dictionary<string, object>>
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["@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<string, object>
|
||||
{
|
||||
["@type"] = "Offer",
|
||||
["price"] = "23.99",
|
||||
["priceCurrency"] = "GBP",
|
||||
["availability"] = "https://schema.org/InStock",
|
||||
["url"] = baseUrl
|
||||
}
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["@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<string, object>
|
||||
{
|
||||
["@type"] = "Offer",
|
||||
["price"] = "17.99",
|
||||
["priceCurrency"] = "GBP",
|
||||
["availability"] = "https://schema.org/InStock",
|
||||
["url"] = baseUrl
|
||||
}
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["@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<string, object>
|
||||
{
|
||||
["@type"] = "Offer",
|
||||
["price"] = "13.99",
|
||||
["priceCurrency"] = "GBP",
|
||||
["availability"] = "https://schema.org/InStock",
|
||||
["url"] = baseUrl
|
||||
}
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["@type"] = "Book",
|
||||
["bookFormat"] = "https://schema.org/EBook",
|
||||
["isbn"] = "978-1-0682258-3-3",
|
||||
["name"] = "The Alpha Flame: Discovery – eBook",
|
||||
["image"] = imageUrl,
|
||||
["offers"] = new Dictionary<string, object>
|
||||
{
|
||||
["@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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -214,11 +214,6 @@ namespace CatherineLynwood.Controllers
|
||||
return View();
|
||||
}
|
||||
|
||||
[Route("reckoning")]
|
||||
public IActionResult Reckoning()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[Route("redemption")]
|
||||
public IActionResult Redemption()
|
||||
|
||||
@ -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<BlockPhpRequestsMiddleware> _logger;
|
||||
private IWebHostEnvironment _environment;
|
||||
|
||||
public BlockPhpRequestsMiddleware(RequestDelegate next, ILogger<BlockPhpRequestsMiddleware> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
namespace CatherineLynwood.Middleware
|
||||
{
|
||||
public class BotFilterMiddleware
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private static readonly List<string> 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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -1,82 +0,0 @@
|
||||
using System.Net;
|
||||
|
||||
namespace CatherineLynwood.Middleware
|
||||
{
|
||||
public class IpqsBlockMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<IpqsBlockMiddleware> _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<IpqsBlockMiddleware> 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<IpqsResponse>(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; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,270 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace CatherineLynwood.Models
|
||||
{
|
||||
public static class BuyCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, BuyLink> 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 : "";
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
71
CatherineLynwood/Models/BuyPanelViewModel.cs
Normal file
71
CatherineLynwood/Models/BuyPanelViewModel.cs
Normal file
@ -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<BuyLink> Links { get; set; } = new List<BuyLink>();
|
||||
|
||||
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<BuyGroup> Groups { get; set; } = new List<BuyGroup>();
|
||||
|
||||
public string ISO2
|
||||
{
|
||||
get { return _iso2; }
|
||||
set { _iso2 = value; }
|
||||
}
|
||||
|
||||
public string Src { get; set; } = string.Empty;
|
||||
|
||||
#endregion Public Properties
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -121,8 +121,6 @@ namespace CatherineLynwood
|
||||
app.UseMiddleware<HoneypotLoggingMiddleware>();
|
||||
app.UseMiddleware<RedirectMiddleware>();
|
||||
app.UseMiddleware<EnsureSidMiddleware>();
|
||||
|
||||
// Resolve ISO2 once per request, stored in HttpContext.Items via GeoResolutionMiddleware
|
||||
app.UseMiddleware<GeoResolutionMiddleware>();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
@ -27,6 +27,76 @@ namespace CatherineLynwood.Services
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
|
||||
public async Task<BuyPanelViewModel> 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<bool> 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<Reviews> GetReviewsAsync()
|
||||
public async Task<Reviews> 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<bool> 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();
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ namespace CatherineLynwood.Services
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GeoIpResult?> ResolveAsync(IPAddress ip, string userAgent, CancellationToken ct = default)
|
||||
public async Task<GeoIpResult?> 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);
|
||||
|
||||
@ -6,6 +6,6 @@ namespace CatherineLynwood.Services
|
||||
{
|
||||
public interface IGeoResolver
|
||||
{
|
||||
Task<GeoIpResult?> ResolveAsync(IPAddress ip, string userAgent, CancellationToken ct = default);
|
||||
Task<GeoIpResult?> ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
134
CatherineLynwood/Views/Collaborations/LarhysaSaddul.cshtml
Normal file
134
CatherineLynwood/Views/Collaborations/LarhysaSaddul.cshtml
Normal file
@ -0,0 +1,134 @@
|
||||
@{
|
||||
ViewData["Title"] = "Larhysa Saddul – Voice of The Alpha Flame: Discovery";
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Larhysa Saddul</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Header / Hero -->
|
||||
<div class="row align-items-center mb-5">
|
||||
<div class="col-lg-7">
|
||||
<h1 class="display-5 fw-bold mb-3">
|
||||
Larhysa Saddul
|
||||
</h1>
|
||||
<p class="lead mb-3">
|
||||
Actress, singer, and voice artist – the voice of
|
||||
<span class="fw-semibold">The Alpha Flame: Discovery</span>.
|
||||
</p>
|
||||
<p class="text-muted mb-4">
|
||||
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.
|
||||
</p>
|
||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-primary me-2 mb-2">
|
||||
About <em>The Alpha Flame: Discovery</em>
|
||||
</a>
|
||||
@* <a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-outline-secondary mb-2">
|
||||
Audiobook information
|
||||
</a> *@
|
||||
|
||||
<!-- Bio section -->
|
||||
<h2 class="h3 mb-3">About Larhysa</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
On stage, she is known for standout roles such as the Evil Stepmother in
|
||||
<em>Cinder’Aliyah</em>, where she performed alongside BBC’s Abdullah Afzal, and for
|
||||
her work in the acclaimed, five-star Edinburgh Fringe production
|
||||
<em>NewsRevue</em>, 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.
|
||||
</p>
|
||||
<p>
|
||||
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 <em>The Alpha Flame: Discovery</em> marks an exciting
|
||||
new chapter in her career.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<responsive-image src="larhysa-saddul.png" alt="Larhysa Saddul" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="100"></responsive-image>
|
||||
<p class="small text-muted mt-2">
|
||||
Images courtesy of Larhysa Saddul, used with permission.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Audio: Interview -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-lg-8 mb-3">
|
||||
<responsive-image src="larhysa-saddul-in-action.png" alt="Larhysa Saddul" class="img-fluid rounded-5 border border-3 border-dark shadow-lg mb-3" display-width-percentage="100"></responsive-image>
|
||||
|
||||
|
||||
<div class="card border-0 bg-dark text-white shadow-sm">
|
||||
<div class="card-body">
|
||||
<h3 class="h5 mb-3">Hear Larhysa in action</h3>
|
||||
<p class="mb-3">
|
||||
Here are a few excerpts from Larhysa's excellent narration of The Alphe Flame: Discovery.
|
||||
</p>
|
||||
<audio controls class="w-100">
|
||||
<source src="~/audio/excerpts-1.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="small text-white mt-2">
|
||||
Excerpt taken from Chapter 25
|
||||
</p>
|
||||
<audio controls class="w-100">
|
||||
<source src="~/audio/excerpts-3.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="small text-white mt-2">
|
||||
Excerpt taken from Chapter 32
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mb-3">
|
||||
<div class="card border-0 bg-light h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<h2 class="h3 mb-3">Interview: Catherine & Larhysa</h2>
|
||||
<p>
|
||||
In this in-depth conversation, author Catherine Lynwood and Larhysa Saddul talk
|
||||
about adapting <em>The Alpha Flame: Discovery</em> for audio, the emotional weight
|
||||
of the story, and the craft that goes into bringing Maggie and Beth to life in
|
||||
the recording booth.
|
||||
</p>
|
||||
<p class="fw-semibold mb-2">
|
||||
Listen to the full interview
|
||||
</p>
|
||||
<audio controls class="w-100">
|
||||
<source src="~/audio/larhysa-Interview.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="small text-muted mt-2 mb-0">
|
||||
Duration: ~25 minutes.
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
273
CatherineLynwood/Views/Discovery/AudioBook.cshtml
Normal file
273
CatherineLynwood/Views/Discovery/AudioBook.cshtml
Normal file
@ -0,0 +1,273 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Audiobook";
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Audiobook</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="row align-items-center g-4 mb-4 mb-lg-5">
|
||||
<div class="col-12 col-lg-7">
|
||||
<h1 class="display-6 fw-bold mb-2">Listen to <span class="text-nowrap">The Alpha Flame: Discovery</span></h1>
|
||||
<p class="lead mb-3">
|
||||
A slow-burn, character-driven story that hits differently through headphones.
|
||||
</p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<span class="badge text-bg-secondary">🎧 17.5 hours</span>
|
||||
<span class="badge text-bg-secondary">🎙️ Fully narrated</span>
|
||||
<span class="badge text-bg-secondary">🔒 Exclusive to Audible</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-lg" target="_blank" href="https://www.audible.co.uk/pd/B0GL4NGG9F/?source_code=AUKFrDlWS02231890H6-BK-ACX0-493566&ref=acx_bty_BK_ACX0_493566_rh_uk" ping="/track/click?slug=audible-uk&src=AudioBook" rel="nofollow noindex">
|
||||
<span><i class="fab fa-amazon me-1"></i> Listen on Audible</span>
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="#clips">
|
||||
Try the clips
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="text-white small mt-3 mb-0">
|
||||
Tip: Try one minute. If the voice doesn’t pull you in, close the page and go back to doomscrolling.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-5">
|
||||
<!-- Optional cover / visual -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<responsive-image src="the-alpha-flame-discovery-cover.png"
|
||||
class="img-fluid rounded"
|
||||
alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse"
|
||||
display-width-percentage="20" style="max-width: 120px;"></responsive-image>
|
||||
<div>
|
||||
<div class="fw-semibold">Narration</div>
|
||||
<div class="small mb-2">Narrated by <strong><a asp-controller="Collaborations" asp-action="LarhysaSaddul">Lahrysa Saddul</a></strong></div>
|
||||
|
||||
<ul class="list-unstyled small mb-0">
|
||||
<li class="mb-1">• Best for: drives, late nights, walks</li>
|
||||
<li class="mb-1">• Style: intimate, story-first, no gimmicks</li>
|
||||
<li class="mb-0">• No spoilers in the clips below</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-dark border rounded-3 mt-3 mb-0">
|
||||
<div class="small">
|
||||
<strong>Already read it?</strong>
|
||||
The audiobook isn’t a repeat, it’s a different experience. Scenes land differently when you can’t rush them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Listening contexts -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2">🎧 Best with headphones</div>
|
||||
<div class="text-muted small">
|
||||
This is the kind of narration where quiet details matter.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2">🌙 Best when it’s late</div>
|
||||
<div class="text-muted small">
|
||||
When your brain is tired and your eyes refuse to read another page.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2">🚗 Best on long drives</div>
|
||||
<div class="text-muted small">
|
||||
Perfect for motorways, rain, and the kind of silence that gets loud.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clips -->
|
||||
<div id="clips" class="mb-4">
|
||||
<div class="d-flex align-items-end justify-content-between flex-wrap gap-2 mb-2">
|
||||
<h2 class="h3 mb-0">Audio clips</h2>
|
||||
<span class="text-white small">Short, spoiler-safe moments to sample the voice and tone.</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Clip card 1 -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-uppercase small text-muted mb-1">Moment 1</div>
|
||||
<h3 class="h5 mb-2">“She saved me.”</h3>
|
||||
<p class="small text-muted mb-3">
|
||||
Shock hasn’t worn off yet. Voices are low. One truth slips out that changes how everyone in the room sees her.
|
||||
</p>
|
||||
|
||||
<audio class="w-100" controls preload="none">
|
||||
<source src="/audio/discovery-clip-1.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2 small text-muted">
|
||||
<span>⏱ 0:41</span>
|
||||
<span>Emotion-led, post-shock</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clip card 2 -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-uppercase small text-muted mb-1">Moment 2</div>
|
||||
<h3 class="h5 mb-2">Voices through a door</h3>
|
||||
<p class="small text-muted mb-3">
|
||||
A party outside. A quiet room inside. And words that were never meant to be overheard.
|
||||
</p>
|
||||
|
||||
<audio class="w-100" controls preload="none">
|
||||
<source src="/audio/discovery-clip-2.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2 small text-muted">
|
||||
<span>⏱ 1:06</span>
|
||||
<span>Rising unease</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clip card 3 -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="text-uppercase small text-muted mb-1">Moment 3</div>
|
||||
<h3 class="h5 mb-2">Betrayal doesn’t shout</h3>
|
||||
<p class="small text-muted mb-3">
|
||||
Accusations come out sideways. Trust fractures in a place no one ever meant to stop.
|
||||
</p>
|
||||
|
||||
<audio class="w-100" controls preload="none">
|
||||
<source src="/audio/discovery-clip-3.mp3" type="audio/mpeg" />
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2 small text-muted">
|
||||
<span>⏱ 0:54</span>
|
||||
<span>Raw, confrontational</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- What you get (audio-specific, not plot blurb) -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3">What this audiobook is</h2>
|
||||
<ul class="mb-0">
|
||||
<li class="mb-2">A story you <strong>live inside</strong>, not one you sprint through.</li>
|
||||
<li class="mb-2">Character-led tension, emotional weight, and atmosphere that builds.</li>
|
||||
<li class="mb-2">Narration that gives scenes room to breathe, pauses included.</li>
|
||||
<li class="mb-0">Ideal if you like long, immersive listens rather than quick hits.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 mb-3">Quick details</h2>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-5 text-muted">Length</dt>
|
||||
<dd class="col-7">17.5 hours</dd>
|
||||
|
||||
<dt class="col-5 text-muted">Format</dt>
|
||||
<dd class="col-7">Audible audiobook</dd>
|
||||
|
||||
<dt class="col-5 text-muted">Availability</dt>
|
||||
<dd class="col-7">Exclusive to Audible</dd>
|
||||
|
||||
<dt class="col-5 text-muted">Best for</dt>
|
||||
<dd class="col-7">Headphones, drives, late nights</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Footer -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3">
|
||||
<div>
|
||||
<div class="h5 mb-1">Ready to listen?</div>
|
||||
<div class="text-muted small mb-0">
|
||||
Click through to Audible. If you’re a member, a credit is usually less painful than buying hardbacks at full price.
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-dark btn-lg" target="_blank" href="https://www.audible.co.uk/pd/B0GL4NGG9F/?source_code=AUKFrDlWS02231890H6-BK-ACX0-493566&ref=acx_bty_BK_ACX0_493566_rh_uk" ping="/track/click?slug=audible-uk&src=AudioBook" rel="nofollow noindex">
|
||||
<span><i class="fab fa-amazon me-1"></i> Listen on Audible</span>
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="#clips">
|
||||
Play a clip
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@section Scripts{
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const audioPlayers = document.querySelectorAll('#clips audio');
|
||||
|
||||
audioPlayers.forEach(player => {
|
||||
player.addEventListener('play', () => {
|
||||
audioPlayers.forEach(otherPlayer => {
|
||||
if (otherPlayer !== player && !otherPlayer.paused) {
|
||||
otherPlayer.pause();
|
||||
otherPlayer.currentTime = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
}
|
||||
@ -1,77 +1,78 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 1 - Drowning in Silence</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 1 - Drowning in Silence - Beth</h1>
|
||||
<p>An exclusive glimpse into Beth's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="beth-stood-in-bathroom.png" alt="Scene from Beth's story" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 1 - Drowning in Silence</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 1 - Drowning in Silence - Beth</h1>
|
||||
<p>An exclusive glimpse into Beth's story</p>
|
||||
</div>
|
||||
|
||||
<div class="ratio ratio-16x9">
|
||||
<video controls="controls" poster="/images/Chapter-1.png" class="rounded-5">
|
||||
<source src="~/videos/Chapter-1-preview.mp4" type="video/mp4"/>
|
||||
</video>
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="beth-stood-in-bathroom.png" alt="Scene from Beth's story" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="text-center text-muted small pt-2">
|
||||
Watch Beth narrating part of Chapter 1 - Drowning in Silence.
|
||||
</p>
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-1.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Beth narrating the complete Chapter 1 - Drowning in Silence.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">Drowning in Silence - Beth</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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...</em>
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
|
||||
<div class="ratio ratio-16x9">
|
||||
<video controls="controls" poster="/images/Chapter-1.png" class="rounded-5">
|
||||
<source src="~/videos/Chapter-1-preview.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
</div>
|
||||
<p class="text-center text-muted small pt-2">
|
||||
Watch Beth narrating part of Chapter 1 - Drowning in Silence.
|
||||
</p>
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-1.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Beth narrating the complete Chapter 1 - Drowning in Silence.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">Drowning in Silence - Beth</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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...</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,92 +2,94 @@
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 13 - A Name She Never Owned</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 13 - A Name She Never Owned - Susie</h1>
|
||||
<p>An exclusive glimpse into Susie's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="pub-from-chapter-13.png" alt="The Pub from Chapter 13" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 13 - A Name She Never Owned</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-13.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned
|
||||
</p>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 13 - A Name She Never Owned - Susie</h1>
|
||||
<p>An exclusive glimpse into Susie's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="pub-from-chapter-13.png" alt="The Pub from Chapter 13" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-13.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">A Name She Never Owned - Susie</p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Hi,” he said, smiling nervously. “I wondered if you’d remember.”</p>
|
||||
<p>“Of course,” I replied, returning his smile.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“These are for you.”</p>
|
||||
<p>“Wow! Thank you,” I said, genuinely blown away. “That’s so nice of you. No one’s ever bought me flowers before.”</p>
|
||||
<p><em>Ben’s smile deepened. “Are you ready?”</em></p>
|
||||
<p>“Sure.”</p>
|
||||
<p><em>He started the car and pulled away, driving slowly, a little nervously.</em></p>
|
||||
<p>“Have you been driving long?” I asked after a few moments.</p>
|
||||
<p><em>Ben glanced across. “About a year. Am I that bad?”</em></p>
|
||||
<p><em>I laughed softly. “No, not at all. It’s just that most guys seem to drive a lot faster.”</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>“Do you live on your own?”</p>
|
||||
<p>“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.”</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>“Thank you,” I said, feeling a blush creep up my cheeks.</p>
|
||||
<p>“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?”</p>
|
||||
<p><em>I smiled, warmed by his awkward honesty. “Suits me.”</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“This looks nice. Have you been here before?” I asked.</p>
|
||||
<p>“No, never. A friend told me about it when I asked him where we could go.”</p>
|
||||
<p><em>Shocked, I stared at him. “You told your mate you’re going on a date with me?”</em></p>
|
||||
<p>“Yeah. What’s wrong with that?”</p>
|
||||
<p>“Did you tell him I’m a prostitute?”</p>
|
||||
<p><em>Ben gave a half-smile. “I thought we agreed what each of us does doesn’t matter?”</em></p>
|
||||
<p>“It doesn’t.”</p>
|
||||
<p>“Exactly. But if you must know, I just told him I had a date with a beautiful girl. That’s all.”</p>
|
||||
<p>“So, you’re embarrassed to be seen with me?”</p>
|
||||
<p><em>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.”</em></p>
|
||||
<p><em>I was quite touched.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Do you want to go and sit down, and I’ll get the drinks?” Ben asked, glancing at the bar.</p>
|
||||
<p>“Yeah, okay. Can I have a vodka and Coke, please?”</p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>It wasn’t long before Ben returned with the drinks. He placed mine carefully in front of me.</em></p>
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">A Name She Never Owned - Susie</p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Hi,” he said, smiling nervously. “I wondered if you’d remember.”</p>
|
||||
<p>“Of course,” I replied, returning his smile.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“These are for you.”</p>
|
||||
<p>“Wow! Thank you,” I said, genuinely blown away. “That’s so nice of you. No one’s ever bought me flowers before.”</p>
|
||||
<p><em>Ben’s smile deepened. “Are you ready?”</em></p>
|
||||
<p>“Sure.”</p>
|
||||
<p><em>He started the car and pulled away, driving slowly, a little nervously.</em></p>
|
||||
<p>“Have you been driving long?” I asked after a few moments.</p>
|
||||
<p><em>Ben glanced across. “About a year. Am I that bad?”</em></p>
|
||||
<p><em>I laughed softly. “No, not at all. It’s just that most guys seem to drive a lot faster.”</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>“Do you live on your own?”</p>
|
||||
<p>“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.”</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>“Thank you,” I said, feeling a blush creep up my cheeks.</p>
|
||||
<p>“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?”</p>
|
||||
<p><em>I smiled, warmed by his awkward honesty. “Suits me.”</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“This looks nice. Have you been here before?” I asked.</p>
|
||||
<p>“No, never. A friend told me about it when I asked him where we could go.”</p>
|
||||
<p><em>Shocked, I stared at him. “You told your mate you’re going on a date with me?”</em></p>
|
||||
<p>“Yeah. What’s wrong with that?”</p>
|
||||
<p>“Did you tell him I’m a prostitute?”</p>
|
||||
<p><em>Ben gave a half-smile. “I thought we agreed what each of us does doesn’t matter?”</em></p>
|
||||
<p>“It doesn’t.”</p>
|
||||
<p>“Exactly. But if you must know, I just told him I had a date with a beautiful girl. That’s all.”</p>
|
||||
<p>“So, you’re embarrassed to be seen with me?”</p>
|
||||
<p><em>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.”</em></p>
|
||||
<p><em>I was quite touched.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Do you want to go and sit down, and I’ll get the drinks?” Ben asked, glancing at the bar.</p>
|
||||
<p>“Yeah, okay. Can I have a vodka and Coke, please?”</p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>It wasn’t long before Ben returned with the drinks. He placed mine carefully in front of me.</em></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,75 +2,77 @@
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 2 - The Last Lesson</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 2- The Last Lesson - Maggie</h1>
|
||||
<p>An exclusive glimpse into Maggie's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="maggie-with-her-tr6-2.png" alt="Maggie With Her TR6" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 2 - The Last Lesson</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-2.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Maggie narrating the complete Chapter 2 - The Last Lesson,
|
||||
</p>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 2- The Last Lesson - Maggie</h1>
|
||||
<p>An exclusive glimpse into Maggie's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="maggie-with-her-tr6-2.png" alt="Maggie With Her TR6" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-2.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Maggie narrating the complete Chapter 2 - The Last Lesson,
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">The Last Lesson - Maggie</p>
|
||||
<p><em>There was a knock at the door.</em></p>
|
||||
<p>“Colin’s here,” called mum.</p>
|
||||
<p>“Okay, I’ll be there in a second,” I replied.</p>
|
||||
<p><em>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,</em> I thought to myself, <em>trying to stay calm.</em></p>
|
||||
<p><em>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.</em> <em>Okay let’s do this</em>, I thought.</p>
|
||||
<p>“Maggie,” called Mum, “you’re going to be late.”</p>
|
||||
<p>“Coming.”</p>
|
||||
<p><em>I walked into the hallway where mum was talking to Colin.</em></p>
|
||||
<p>“How do I look?” I asked.</p>
|
||||
<p>Mum looked at me. “No bra?” she queried.</p>
|
||||
<p><em>I just looked at her, and mum smiled in response.</em></p>
|
||||
<p>Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.</p>
|
||||
<p>“Good luck, Sweetie,” said Mum.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Thank you,” I said. “What’s with the chivalry?”</p>
|
||||
<p>“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.</p>
|
||||
<p><em>I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.</em></p>
|
||||
<p>“Just take your time,” said Colin.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.</p>
|
||||
<p>“Sorry, I’m rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.</p>
|
||||
<p><em>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.</em> <em>Pull yourself together</em>, I thought.</p>
|
||||
<p>Calmly, he said, “Don’t worry. Let’s just try that again...”</p>
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">The Last Lesson - Maggie</p>
|
||||
<p><em>There was a knock at the door.</em></p>
|
||||
<p>“Colin’s here,” called mum.</p>
|
||||
<p>“Okay, I’ll be there in a second,” I replied.</p>
|
||||
<p><em>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,</em> I thought to myself, <em>trying to stay calm.</em></p>
|
||||
<p><em>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.</em> <em>Okay let’s do this</em>, I thought.</p>
|
||||
<p>“Maggie,” called Mum, “you’re going to be late.”</p>
|
||||
<p>“Coming.”</p>
|
||||
<p><em>I walked into the hallway where mum was talking to Colin.</em></p>
|
||||
<p>“How do I look?” I asked.</p>
|
||||
<p>Mum looked at me. “No bra?” she queried.</p>
|
||||
<p><em>I just looked at her, and mum smiled in response.</em></p>
|
||||
<p>Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.</p>
|
||||
<p>“Good luck, Sweetie,” said Mum.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Thank you,” I said. “What’s with the chivalry?”</p>
|
||||
<p>“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.</p>
|
||||
<p><em>I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.</em></p>
|
||||
<p>“Just take your time,” said Colin.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.</p>
|
||||
<p>“Sorry, I’m rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.</p>
|
||||
<p><em>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.</em> <em>Pull yourself together</em>, I thought.</p>
|
||||
<p>Calmly, he said, “Don’t worry. Let’s just try that again...”</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 @@
|
||||
}</style>
|
||||
</noscript>
|
||||
|
||||
<!-- Buy Box -->
|
||||
@* buyBox: server-side slugs + <a ping> 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";
|
||||
}
|
||||
|
||||
<partial name="_BuyBox" />
|
||||
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -83,6 +72,28 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card character-card">
|
||||
<div class="card-body">
|
||||
<h3 class="h6 text-uppercase text-muted mb-2">★ Audiobook Out Now ★</h3>
|
||||
|
||||
<p class="mb-3">
|
||||
<em>The Alpha Flame: Discovery</em> 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.
|
||||
</p>
|
||||
|
||||
<p class="mb-3">
|
||||
Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen.
|
||||
</p>
|
||||
|
||||
<a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-dark">
|
||||
🎧 Explore the audiobook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Social proof: show one standout review near the top -->
|
||||
@if (showReviews)
|
||||
{
|
||||
@ -144,7 +155,7 @@
|
||||
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
|
||||
<p class="mb-0">Survival, secrets, and sisters in 1983 Birmingham.</p>
|
||||
<p class="mb-0">Survival, secrets, and shadows in 1983 Birmingham.</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body" id="synopsis-body">
|
||||
@ -165,6 +176,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Teaser -->
|
||||
<p class="card-text">
|
||||
She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away.
|
||||
</p>
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
@ -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 @@
|
||||
|
||||
<!-- HERO: Cover + Trailer + Buy Box -->
|
||||
<section class="mb-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
|
||||
<p class="mb-0">Birmingham 1983 - A tale of survival, secrets, and sisterhood.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 align-items-stretch">
|
||||
<!-- Trailer + Buy Box -->
|
||||
<div class="col-12">
|
||||
@ -50,18 +56,7 @@
|
||||
}</style>
|
||||
</noscript>
|
||||
|
||||
<!-- Buy Box -->
|
||||
@* buyBox: server-side slugs + <a ping> 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";
|
||||
}
|
||||
|
||||
<partial name="_BuyBox" />
|
||||
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src })
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -77,6 +72,27 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card character-card">
|
||||
<div class="card-body">
|
||||
<h3 class="h6 text-uppercase text-muted mb-2">★ Audiobook Out Now ★</h3>
|
||||
|
||||
<p class="mb-3">
|
||||
<em>The Alpha Flame: Discovery</em> 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.
|
||||
</p>
|
||||
|
||||
<p class="mb-3">
|
||||
Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen.
|
||||
</p>
|
||||
|
||||
<a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-dark">
|
||||
🎧 Explore the audiobook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Social proof: show one standout review near the top -->
|
||||
@if (showReviews)
|
||||
{
|
||||
@ -136,46 +152,44 @@
|
||||
<!-- Synopsis: open on mobile, collapsible on desktop -->
|
||||
<section id="synopsis" class="mb-4">
|
||||
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
|
||||
<p class="mb-0">Survival, secrets, and sisters in 1983 Birmingham.</p>
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
|
||||
<p class="mb-0">Birmingham 1983 - A tale of survival, secrets, and sisterhood.</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body" id="synopsis-body">
|
||||
<!-- Audio blurb -->
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col-2">
|
||||
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="audio-player text-center">
|
||||
<audio id="player">
|
||||
<source src="/audio/the-alpha-flame-discovery-catherine.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
<p class="text-center text-white small mb-0">Listen to Catherine talking about the book</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full synopsis -->
|
||||
<div>
|
||||
<div class="mt-2">
|
||||
<p class="card-text">She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body" id="synopsis-body">
|
||||
<!-- Audio blurb -->
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col-2">
|
||||
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="audio-player text-center">
|
||||
<audio id="player">
|
||||
<source src="/audio/the-alpha-flame-discovery-catherine.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
<p class="text-center text-white small mb-0">Listen to Catherine talking about the book</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teaser -->
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<!-- One copy of the full synopsis -->
|
||||
<div>
|
||||
<div class="mt-2">
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
@ -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 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card character-card">
|
||||
<div class="card-body">
|
||||
<h3 class="h6 text-uppercase text-muted mb-2">★ Audiobook Out Now ★</h3>
|
||||
|
||||
<p class="mb-3">
|
||||
<em>The Alpha Flame: Discovery</em> 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.
|
||||
</p>
|
||||
|
||||
<p class="mb-3">
|
||||
Ideal for long drives, late nights, or anyone who prefers to sink into a story through headphones rather than skim it on a screen.
|
||||
</p>
|
||||
|
||||
<a asp-controller="Discovery" asp-action="AudioBook" class="btn btn-dark">
|
||||
🎧 Explore the audiobook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Synopsis first (story-first layout) -->
|
||||
<section id="synopsis" class="mb-4">
|
||||
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
|
||||
@ -80,14 +101,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teaser -->
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<!-- One copy of the full synopsis -->
|
||||
<!-- Full synopsis -->
|
||||
<div>
|
||||
<div class="mt-2">
|
||||
<p class="card-text">She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
@ -161,17 +180,7 @@
|
||||
<div class="col-12 d-flex">
|
||||
<div class="card character-card h-100 flex-fill" id="buy-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
@* buyBox: server-side slugs + <a ping> 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";
|
||||
}
|
||||
|
||||
<partial name="_BuyBox" />
|
||||
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Discovery</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Trailer (Variant D) -->
|
||||
<section id="interactive-trailer" class="my-4">
|
||||
<div class="card character-card overflow-hidden">
|
||||
<div class="it-stage" id="itStage" aria-live="polite" aria-label="Interactive trailer">
|
||||
<!-- Background image -->
|
||||
<img id="itBgA" class="it-bg show" alt="" src="/images/webp/the-alpha-flame-discovery-cover-1200.webp" />
|
||||
<img id="itBgB" class="it-bg" alt="" />
|
||||
|
||||
<!-- Detail image (hotspot reveal) -->
|
||||
<img id="itDetail" class="it-detail d-none" alt="" />
|
||||
|
||||
<!-- Timed text container -->
|
||||
<div id="itText" class="it-text"></div>
|
||||
|
||||
<!-- Hotspot layer -->
|
||||
<div id="itHotspots" class="it-hotspots" aria-hidden="false"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tap-to-begin overlay (autoplay unlock) -->
|
||||
<div class="it-overlay" id="itOverlay" role="dialog" aria-modal="true">
|
||||
<div class="it-overlay-inner text-center">
|
||||
<h3 id="itOverlayHeadline" class="mb-2">Do you dare to listen?</h3>
|
||||
<p id="itOverlaySub" class="text-muted mb-3">Tap to begin your 60-second descent.</p>
|
||||
<button id="itBegin" class="btn btn-dark btn-sm">Begin</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="it-progress" aria-hidden="true">
|
||||
<div class="it-progress-bar" id="itProgress"></div>
|
||||
</div>
|
||||
|
||||
<!-- End CTA (shown when timeline completes) -->
|
||||
<div class="it-end d-none" id="itEnd" aria-live="polite">
|
||||
<h3 id="itEndHeadline" class="mb-2">The story starts here.</h3>
|
||||
<a id="itEndBtn" href="#buyBox" class="btn btn-dark btn-sm">Buy now</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Synopsis first (story-first layout) -->
|
||||
<section id="synopsis" class="mb-4">
|
||||
<div class="card character-card text-white" style="background: url('/images/webp/synopsis-background-960.webp'); background-position: center; background-size: cover;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Discovery</span></h2>
|
||||
<p class="mb-0">Survival, secrets, and sisters in 1983 Birmingham.</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body" id="synopsis-body">
|
||||
<!-- Audio blurb -->
|
||||
<div class="row align-items-center mb-3">
|
||||
<div class="col-2">
|
||||
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="20"></responsive-image>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<div class="audio-player text-center">
|
||||
<audio id="player">
|
||||
<source src="/audio/the-alpha-flame-discovery-catherine.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
<p class="text-center text-white small mb-0">Listen to Catherine talking about the book</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teaser -->
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<!-- One copy of the full synopsis -->
|
||||
<div>
|
||||
<div class="mt-2">
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
<p class="card-text">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.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Social proof near top -->
|
||||
@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");
|
||||
|
||||
<section class="mb-4">
|
||||
<div class="card character-card">
|
||||
<div class="card-body">
|
||||
<h3 class="h6 text-uppercase text-muted mb-2">★ Reader Praise ★</h3>
|
||||
<blockquote class="blockquote mb-2">
|
||||
<span class="mb-2 text-warning d-inline-block">
|
||||
@for (int i = 0; i < fullStars; i++)
|
||||
{
|
||||
<i class="fad fa-star"></i>
|
||||
}
|
||||
@if (hasHalfStar)
|
||||
{
|
||||
<i class="fad fa-star-half-alt"></i>
|
||||
}
|
||||
@for (int i = 0; i < emptyStars; i++)
|
||||
{
|
||||
<i class="fad fa-star" style="--fa-primary-opacity:0.2;--fa-secondary-opacity:0.2;"></i>
|
||||
}
|
||||
</span>
|
||||
@Html.Raw(top.ReviewBody)
|
||||
<footer>
|
||||
@top.AuthorName on
|
||||
<cite title="@top.SiteName">
|
||||
@if (string.IsNullOrEmpty(top.URL))
|
||||
{
|
||||
@top.SiteName
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="@top.URL" target="_blank">@top.SiteName</a>
|
||||
}
|
||||
</cite>
|
||||
<span class="text-muted smaller">, @reviewDate</span>
|
||||
</footer>
|
||||
</blockquote>
|
||||
@if (Model.Reviews.Items.Count > 1)
|
||||
{
|
||||
<div class="text-end">
|
||||
<a asp-action="Reviews" class="btn btn-outline-secondary btn-sm">Read more reviews</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Buy Box now AFTER synopsis/review -->
|
||||
<section class="mb-4" id="buy">
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex">
|
||||
<div class="card character-card h-100 flex-fill" id="buy-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
@* buyBox: server-side slugs + <a ping> 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";
|
||||
}
|
||||
|
||||
<partial name="_BuyBox" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sticky mobile buy bar (global, still points to #buyBox inside the partial) -->
|
||||
<div id="mobileBuyBar" class="d-md-none fixed-bottom bg-dark text-white py-2 border-top border-3 border-light" style="z-index:1030;">
|
||||
<div class="container d-flex justify-content-between align-items-center">
|
||||
<span class="small">The Alpha Flame: Discovery</span>
|
||||
<a href="#buyBox" class="btn btn-light btn-sm">Buy now</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chapter Previews -->
|
||||
<section id="chapters" class="mt-4">
|
||||
<h2>Chapter Previews</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 character-card">
|
||||
<a asp-action="Chapter1">
|
||||
<responsive-image src="beth-stood-in-bathroom.png" class="card-img-top" alt="Beth's Bathroom" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
<div class="card-body border-top border-3 border-dark">
|
||||
<h3 class="card-title">Chapter 1, Drowning in Silence, Beth</h3>
|
||||
<p class="card-text">Beth returns home to find her mother lifeless in the bath...</p>
|
||||
<div class="text-end"><a asp-action="Chapter1" class="btn btn-dark">Read More</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 character-card">
|
||||
<a asp-action="Chapter2">
|
||||
<responsive-image src="maggie-with-her-tr6-2.png" class="fit-image" alt="Maggie With Her TR6" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
<div class="card-body border-top border-3 border-dark">
|
||||
<h3 class="card-title">Chapter 2, The Last Lesson, Maggie</h3>
|
||||
<p class="card-text">On Christmas Eve, Maggie nervously heads out for her driving test not knowing the story that will pan out before her...</p>
|
||||
<div class="text-end"><a asp-action="Chapter2" class="btn btn-dark">Read More</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 character-card">
|
||||
<a asp-action="Chapter13">
|
||||
<responsive-image src="pub-from-chapter-13.png" class="fit-image" alt="Pub from Chapter 13" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
<div class="card-body border-top border-3 border-dark">
|
||||
<h3 class="card-title">Chapter 13, A Name She Never Owned, Susie</h3>
|
||||
<p class="card-text">Susie goes out for a drink with a punter. What on earth could go wrong...</p>
|
||||
<div class="text-end"><a asp-action="Chapter13" class="btn btn-dark">Read More</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<!-- Plyr for audio -->
|
||||
<script>
|
||||
const player = new Plyr('audio');
|
||||
</script>
|
||||
|
||||
<script type="application/json" id="itConfig">
|
||||
{
|
||||
"version": 1,
|
||||
"cover": {
|
||||
"mobileSrc": "/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||
"desktopSrc": "/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||
"alt": "The Alpha Flame: Discovery by Catherine Lynwood"
|
||||
},
|
||||
"coverHoldMs": 2200,
|
||||
"tapToBegin": {
|
||||
"enabled": true,
|
||||
"headline": "Do you dare to listen?",
|
||||
"subline": "Tap to begin your 60-second descent."
|
||||
},
|
||||
"endCta": {
|
||||
"headline": "The story starts here.",
|
||||
"buttonText": "Buy now",
|
||||
"buttonHref": "#buyBox"
|
||||
},
|
||||
"segments": [
|
||||
{
|
||||
"id": "beth-hallway",
|
||||
"background": {
|
||||
"mobileSrc": "/images/webp/a-letter-to-readers-before-launch-960.webp",
|
||||
"desktopSrc": "/images/webp/a-letter-to-readers-before-launch-960.webp",
|
||||
"alt": "Dim hallway inside Beth's flat"
|
||||
},
|
||||
"audio": { "src": "/audio/trailer/test.mp3", "gain": 1.0 },
|
||||
"durationMs": 12000,
|
||||
"text": {
|
||||
"position": "bottom",
|
||||
"align": "center",
|
||||
"lines": [
|
||||
{ "t": "The water had gone cold.", "atMs": 300, "outMs": 3500 },
|
||||
{ "t": "She hadn’t moved.", "atMs": 3800, "outMs": 7800 }
|
||||
]
|
||||
},
|
||||
"hotspots": [
|
||||
{
|
||||
"id": "door",
|
||||
"rectPct": { "x": 58, "y": 36, "w": 18, "h": 32 },
|
||||
"detail": {
|
||||
"mobileSrc": "/images/webp/art-of-pacing-a-thriller-1200.webp",
|
||||
"desktopSrc": "/images/webp/art-of-pacing-a-thriller-1200.webp",
|
||||
"alt": "Beth's bedroom beyond the doorway",
|
||||
"holdMs": 2800
|
||||
},
|
||||
"hints": { "pulse": true, "label": "Look closer" },
|
||||
"analyticsLabel": "hs_beth_door"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bathroom-mirror",
|
||||
"background": {
|
||||
"mobileSrc": "/images/webp/asking-for-reviews-1200.webp",
|
||||
"desktopSrc": "/images/webp/asking-for-reviews-1200.webp",
|
||||
"alt": "Steamed bathroom mirror"
|
||||
},
|
||||
"audio": { "src": "/audio/trailer/test.mp3" },
|
||||
"durationMs": 12000,
|
||||
"text": {
|
||||
"position": "middle",
|
||||
"align": "center",
|
||||
"lines": [
|
||||
{ "t": "If I tell you this, you will not think less of me, will you…", "atMs": 300, "outMs": 5500 }
|
||||
]
|
||||
},
|
||||
"hotspots": [
|
||||
{
|
||||
"id": "mirror-smudge",
|
||||
"rectPct": { "x": 40, "y": 25, "w": 20, "h": 18 },
|
||||
"detail": {
|
||||
"mobileSrc": "/images/webp/beth-6-1200.webp",
|
||||
"desktopSrc": "/images/webp/beth-6-1200.webp",
|
||||
"alt": "Handprint on the mirror",
|
||||
"holdMs": 2200
|
||||
},
|
||||
"analyticsLabel": "hs_mirror"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "maggie-road",
|
||||
"background": {
|
||||
"mobileSrc": "/images/webp/beth-9-1200.webp",
|
||||
"desktopSrc": "/images/webp/beth-9-1200.webp",
|
||||
"alt": "Headlights on a dark road"
|
||||
},
|
||||
"audio": { "src": "/audio/trailer/test.mp3" },
|
||||
"durationMs": 12000,
|
||||
"text": {
|
||||
"position": "top",
|
||||
"align": "left",
|
||||
"lines": [
|
||||
{ "t": "Some people survive.", "atMs": 300, "outMs": 2600 },
|
||||
{ "t": "She lives.", "atMs": 2800, "outMs": 5200 }
|
||||
]
|
||||
},
|
||||
"hotspots": [
|
||||
{
|
||||
"id": "headlights",
|
||||
"rectPct": { "x": 18, "y": 52, "w": 28, "h": 22 },
|
||||
"detail": {
|
||||
"mobileSrc": "/images/webp/beth-11-1200.webp",
|
||||
"desktopSrc": "/images/webp/beth-11-1200.webp",
|
||||
"alt": "Dashboard POV racing forward",
|
||||
"holdMs": 2600
|
||||
},
|
||||
"analyticsLabel": "hs_headlights"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "club-neon",
|
||||
"background": {
|
||||
"mobileSrc": "/images/webp/beth-12-1200.webp",
|
||||
"desktopSrc": "/images/webp/beth-12-1200.webp",
|
||||
"alt": "Neon sign outside a club"
|
||||
},
|
||||
"audio": { "src": "/audio/trailer/test.mp3" },
|
||||
"durationMs": 12000,
|
||||
"text": {
|
||||
"position": "bottom",
|
||||
"align": "center",
|
||||
"lines": [
|
||||
{ "t": "Secrets buried for years are about to set this city on fire.", "atMs": 300, "outMs": 6500 }
|
||||
]
|
||||
},
|
||||
"hotspots": [
|
||||
{
|
||||
"id": "neon",
|
||||
"rectPct": { "x": 62, "y": 22, "w": 22, "h": 20 },
|
||||
"detail": {
|
||||
"mobileSrc": "/images/webp/beth-1200.webp",
|
||||
"desktopSrc": "/images/webp/beth-1200.webp",
|
||||
"alt": "Inside the club, smoky silhouettes",
|
||||
"holdMs": 2200
|
||||
},
|
||||
"analyticsLabel": "hs_neon"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
;(function IT_Run(){
|
||||
"use strict";
|
||||
|
||||
// --- utils ---
|
||||
const byId=(id)=>document.getElementById(id);
|
||||
const wait=(ms)=>new Promise(r=>setTimeout(r,ms));
|
||||
const isDesk=()=>window.matchMedia("(min-width: 992px)").matches;
|
||||
const pickSrc=(o)=>!o?null:(o.mobileSrc||o.desktopSrc)?(isDesk()?(o.desktopSrc||o.mobileSrc):(o.mobileSrc||o.desktopSrc)):(o.src||null);
|
||||
|
||||
// --- config ---
|
||||
const cfgEl=byId("itConfig"); if(!cfgEl){console.warn("[IT] Missing #itConfig");return;}
|
||||
let cfg={}; try{cfg=JSON.parse(cfgEl.textContent||"{}");}catch(e){console.error("[IT] Bad JSON",e);return;}
|
||||
|
||||
// --- dom ---
|
||||
const stage=byId("itStage"), bgA=byId("itBgA"), bgB=byId("itBgB"),
|
||||
detail=byId("itDetail"), textBox=byId("itText"), hotspots=byId("itHotspots"),
|
||||
overlay=byId("itOverlay"), beginBtn=byId("itBegin"), progress=byId("itProgress"),
|
||||
endWrap=byId("itEnd"), endH=byId("itEndHeadline"), endBtn=byId("itEndBtn"),
|
||||
ovH=byId("itOverlayHeadline"), ovS=byId("itOverlaySub");
|
||||
|
||||
if(!stage||!bgA||!bgB||!detail||!textBox||!hotspots||!overlay||!beginBtn||!progress||!endWrap){
|
||||
console.error("[IT] Required nodes missing"); return;
|
||||
}
|
||||
|
||||
// crossfade buffers
|
||||
let activeBg=bgA, idleBg=bgB;
|
||||
|
||||
// show cover behind overlay (no black)
|
||||
const initialCover=pickSrc(cfg.cover||{})||activeBg.getAttribute("src");
|
||||
if(initialCover){
|
||||
[bgA,bgB].forEach(img=>{img.loading="eager"; img.decoding="async";});
|
||||
activeBg.src=initialCover;
|
||||
if(cfg.cover?.alt) activeBg.alt=cfg.cover.alt;
|
||||
activeBg.classList.add("show");
|
||||
idleBg.classList.remove("show");
|
||||
}
|
||||
|
||||
// overlay + CTA text
|
||||
if(ovH) ovH.textContent=cfg.tapToBegin?.headline||"Tap to begin";
|
||||
if(ovS) ovS.textContent=cfg.tapToBegin?.subline||"";
|
||||
if(cfg.tapToBegin?.enabled===false) overlay.classList.add("hidden");
|
||||
if(cfg.endCta?.headline) endH.textContent=cfg.endCta.headline;
|
||||
if(cfg.endCta?.buttonText) endBtn.textContent=cfg.endCta.buttonText;
|
||||
if(cfg.endCta?.buttonHref) endBtn.href=cfg.endCta.buttonHref;
|
||||
|
||||
// preload
|
||||
const audioMap=new Map();
|
||||
async function preloadAudios(){
|
||||
const segs=cfg.segments||[];
|
||||
await Promise.all(segs.map((seg,i)=>new Promise(res=>{
|
||||
const key=seg.id||`seg_${i}`, src=seg.audio?.src; if(!src){res();return;}
|
||||
const a=new Audio(src); a.preload="auto";
|
||||
const done=()=>res(); a.addEventListener("loadedmetadata",done,{once:true});
|
||||
a.addEventListener("error",done,{once:true}); audioMap.set(key,a); a.load();
|
||||
})));
|
||||
}
|
||||
function preloadImages(){
|
||||
const urls=[], add=u=>{if(u&&!urls.includes(u)) urls.push(u);};
|
||||
add(pickSrc(cfg.cover||{}));
|
||||
(cfg.segments||[]).forEach(seg=>{ add(pickSrc(seg.background||{})); (seg.hotspots||[]).forEach(h=>add(pickSrc(h.detail||{}))); });
|
||||
urls.forEach(u=>{const im=new Image(); im.src=u;});
|
||||
}
|
||||
|
||||
// durations + progress
|
||||
function estimateDurations(){
|
||||
const segs=cfg.segments||[];
|
||||
const durs=segs.map((seg,i)=>{
|
||||
if(typeof seg.durationMs==="number") return seg.durationMs;
|
||||
const a=audioMap.get(seg.id||`seg_${i}`);
|
||||
return (a && isFinite(a.duration) && a.duration>0)? Math.round(a.duration*1000)+350 : 10000;
|
||||
});
|
||||
const coverHold=Math.max(0,cfg.coverHoldMs||0);
|
||||
const total=coverHold+durs.reduce((a,b)=>a+b,0);
|
||||
return {durs,total,coverHold};
|
||||
}
|
||||
const prog={id:0};
|
||||
function startProgress(totalMs){
|
||||
const t0=performance.now();
|
||||
const tick=()=>{const p=(performance.now()-t0)/totalMs*100; progress.style.width=Math.max(0,Math.min(100,p))+"%";
|
||||
if(p<100) prog.id=requestAnimationFrame(tick);
|
||||
};
|
||||
prog.id=requestAnimationFrame(tick);
|
||||
}
|
||||
function stopProgress(){ cancelAnimationFrame(prog.id); }
|
||||
|
||||
// text + hotspots
|
||||
function clearNode(el){ while(el.firstChild) el.removeChild(el.firstChild); }
|
||||
function scheduleText(seg){
|
||||
textBox.className="it-text";
|
||||
textBox.classList.add(seg.text?.position||"bottom");
|
||||
textBox.classList.add(seg.text?.align||"center");
|
||||
const timers=[]; clearNode(textBox);
|
||||
(seg.text?.lines||[]).forEach(line=>{
|
||||
const p=document.createElement("p"); p.className="line"; p.textContent=line.t||""; textBox.appendChild(p);
|
||||
timers.push(setTimeout(()=>p.classList.add("show"), Math.max(0,line.atMs||0)));
|
||||
if(typeof line.outMs==="number") timers.push(setTimeout(()=>p.classList.remove("show"), Math.max(0,line.outMs)));
|
||||
});
|
||||
return ()=>timers.forEach(t=>clearTimeout(t));
|
||||
}
|
||||
function pctToRect(pct, c){ const w=c.clientWidth,h=c.clientHeight;
|
||||
return{left:(pct.x/100)*w, top:(pct.y/100)*h, width:(pct.w/100)*w, height:(pct.h/100)*h};
|
||||
}
|
||||
function renderHotspots(seg){
|
||||
clearNode(hotspots);
|
||||
if(!seg.hotspots||!seg.hotspots.length) return;
|
||||
const btns=[];
|
||||
seg.hotspots.forEach(h=>{
|
||||
const btn=document.createElement("button"); btn.type="button";
|
||||
btn.setAttribute("aria-label", h.hints?.label||"Reveal detail");
|
||||
if(h.hints?.pulse!==false) btn.classList.add("pulse");
|
||||
let hint; if(h.hints?.label){ hint=document.createElement("span"); hint.className="hint"; hint.textContent=h.hints.label; btn.appendChild(hint); }
|
||||
const place=()=>{ const r=pctToRect(h.rectPct,stage);
|
||||
btn.style.left=r.left+"px"; btn.style.top=r.top+"px"; btn.style.width=r.width+"px"; btn.style.height=r.height+"px"; };
|
||||
place(); window.addEventListener("resize", place);
|
||||
btn.addEventListener("click", ()=>{
|
||||
const dSrc=pickSrc(h.detail||{}); if(!dSrc) return;
|
||||
detail.src=dSrc; if(h.detail?.alt) detail.alt=h.detail.alt;
|
||||
detail.classList.remove("d-none"); detail.style.opacity="0";
|
||||
requestAnimationFrame(()=>{ detail.style.opacity="1"; });
|
||||
const hold=Math.max(800,h.detail?.holdMs||2000);
|
||||
setTimeout(()=>{ detail.style.opacity="0"; setTimeout(()=>detail.classList.add("d-none"),450); }, hold);
|
||||
}, {passive:true});
|
||||
hotspots.appendChild(btn); btns.push({hint});
|
||||
});
|
||||
// brief hint reveal
|
||||
btns.forEach(({hint})=>{ if(hint) hint.style.opacity="1"; });
|
||||
setTimeout(()=>btns.forEach(({hint})=>{ if(hint) hint.style.opacity=""; }), 1800);
|
||||
}
|
||||
|
||||
// --- CROSSFADE: wait for decode() to avoid flashes ---
|
||||
async function setBackground(segBg){
|
||||
const src=pickSrc(segBg||{}); if(!src) return;
|
||||
const next=idleBg, current=activeBg;
|
||||
|
||||
// prepare next layer hidden
|
||||
next.classList.remove("show");
|
||||
|
||||
// set source
|
||||
if(next.src!==src){
|
||||
next.src=src;
|
||||
if(segBg?.alt) next.alt=segBg.alt||"";
|
||||
}
|
||||
|
||||
// ensure pixel-ready before swapping (prevents flicker)
|
||||
try{
|
||||
if(next.decode) { await next.decode(); }
|
||||
else {
|
||||
// fallback: if already complete, continue; otherwise wait for load
|
||||
if(!next.complete) await new Promise(res=>{ next.addEventListener("load", ()=>res(), {once:true}); });
|
||||
}
|
||||
}catch(_e){ /* decode can reject on cached images in some browsers; ignore */ }
|
||||
|
||||
// do the crossfade on the next frame
|
||||
requestAnimationFrame(()=>{
|
||||
next.classList.add("show");
|
||||
current.classList.remove("show");
|
||||
});
|
||||
|
||||
// swap buffers
|
||||
activeBg=next; idleBg=current;
|
||||
}
|
||||
|
||||
async function runSegments(durs){
|
||||
const segs=cfg.segments||[];
|
||||
for(let i=0;i<segs.length;i++){
|
||||
const seg=segs[i], key=seg.id||`seg_${i}`;
|
||||
await setBackground(seg.background);
|
||||
renderHotspots(seg);
|
||||
|
||||
const a=audioMap.get(key);
|
||||
if(a){ try{ a.currentTime=0; a.volume=Math.max(0,Math.min(1, seg.audio?.gain??1.0)); await a.play(); }catch{} }
|
||||
|
||||
const clearTimers=scheduleText(seg);
|
||||
await wait(durs[i]||10000);
|
||||
clearTimers(); clearNode(hotspots); if(a) a.pause(); detail.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
|
||||
async function run(){
|
||||
overlay.classList.add("hidden");
|
||||
await preloadAudios(); preloadImages();
|
||||
const {durs,total,coverHold}=estimateDurations();
|
||||
startProgress(total);
|
||||
await wait(coverHold);
|
||||
await runSegments(durs);
|
||||
stopProgress();
|
||||
endWrap.classList.remove("d-none");
|
||||
}
|
||||
|
||||
beginBtn.addEventListener("click", run, {passive:true});
|
||||
overlay.addEventListener("click", (e)=>{ if(e.target===overlay) run(); }, {passive:true});
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
|
||||
meta-description="A gritty 1980s Birmingham psycological crime novel about two girls uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching... discover The Alpha Flame today."
|
||||
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
|
||||
meta-author="Catherine Lynwood"
|
||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
|
||||
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-discovery-cover.png"
|
||||
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
|
||||
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
|
||||
article-published-time="@new DateTime(2024, 11, 20)"
|
||||
article-modified-time="@new DateTime(2025, 09, 10)"
|
||||
twitter-card-type="summary_large_image"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
@Html.Raw(Model.Reviews.SchemaJsonLd)
|
||||
</script>
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
|
||||
<div id="buyBox" class="border border-2 border-dark rounded-4 p-3 bg-light mt-auto">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||||
<h3 class="h5 mb-2 mb-sm-0">Buy the Book</h3>
|
||||
<small id="buyCountryHint" class="text-muted d-flex align-items-center">
|
||||
<img id="buyCountryFlag"
|
||||
class="me-1"
|
||||
alt=""
|
||||
width="20"
|
||||
height="14"
|
||||
loading="lazy"
|
||||
src="@flagSvg"
|
||||
onerror="this.onerror=null;this.src='@flagPng';" />
|
||||
<span id="buyCountryText">Best options for @iso2</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@* ---------------------------
|
||||
Row 1: Direct via printers (GB/US only)
|
||||
--------------------------- *@
|
||||
@if (L.IngramHardback != null || L.IngramPaperback != null)
|
||||
{
|
||||
<div id="rowDirect" class="mb-3">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span>Best price direct from our printers</span>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
@if (L.IngramHardback != null)
|
||||
{
|
||||
<div class="col-12 col-sm-6">
|
||||
<a class="btn btn-dark w-100"
|
||||
href="@L.IngramHardback.Url"
|
||||
ping="@($"/track/click?slug={L.IngramHardback.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-gem me-1"></i> Hardback, direct
|
||||
@if (!string.IsNullOrWhiteSpace(L.IngramHardbackPrice))
|
||||
{
|
||||
<span class="price-chip ms-2">@L.IngramHardbackPrice</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@if (L.IngramPaperback != null)
|
||||
{
|
||||
<div class="col-12 col-sm-6">
|
||||
<a class="btn btn-dark w-100"
|
||||
href="@L.IngramPaperback.Url"
|
||||
ping="@($"/track/click?slug={L.IngramPaperback.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-book me-1"></i> Paperback, direct
|
||||
@if (!string.IsNullOrWhiteSpace(L.IngramPaperbackPrice))
|
||||
{
|
||||
<span class="price-chip ms-2">@L.IngramPaperbackPrice</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span>From other retailers</span>
|
||||
</div>
|
||||
|
||||
@* ---------------------------
|
||||
Row 2: Amazon (always present)
|
||||
--------------------------- *@
|
||||
<div id="rowAmazon" class="mb-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-sm-6">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.AmazonHardback.Url"
|
||||
ping="@($"{pingBase}?slug={L.AmazonHardback.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-gem me-1"></i> Hardback, Amazon
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.AmazonPaperback.Url"
|
||||
ping="@($"{pingBase}?slug={L.AmazonPaperback.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-book me-1"></i> Paperback, Amazon
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ---------------------------
|
||||
Row 3: National retailer (conditional)
|
||||
--------------------------- *@
|
||||
@if (L.NationalHardback != null || L.NationalPaperback != null)
|
||||
{
|
||||
<div id="rowNational" class="mb-2">
|
||||
<div class="row g-2">
|
||||
@if (L.NationalHardback != null)
|
||||
{
|
||||
<div class="col-12 col-sm-6">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.NationalHardback.Url"
|
||||
ping="@($"{pingBase}?slug={L.NationalHardback.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-gem me-1"></i> Hardback, @(L.NationalLabel ?? "National")
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@if (L.NationalPaperback != null)
|
||||
{
|
||||
<div class="col-12 col-sm-6">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.NationalPaperback.Url"
|
||||
ping="@($"{pingBase}?slug={L.NationalPaperback.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-book-open me-1"></i> Paperback, @(L.NationalLabel ?? "National")
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ---------------------------
|
||||
Row 4: eBook stores (Apple, Kobo, Kindle)
|
||||
--------------------------- *@
|
||||
<div id="rowEbooks" class="mb-2">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span>Choose your preferred e-book store</span>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-sm-4">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.Apple.Url"
|
||||
ping="@($"{pingBase}?slug={L.Apple.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fab fa-apple me-1"></i> Apple Books
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.Kobo.Url"
|
||||
ping="@($"{pingBase}?slug={L.Kobo.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fad fa-book-open me-1"></i> Kobo
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<a class="btn btn-outline-dark w-100"
|
||||
href="@L.AmazonKindle.Url"
|
||||
ping="@($"{pingBase}?slug={L.AmazonKindle.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<i class="fab fa-amazon me-1"></i> Kindle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<a asp-action="HowToBuy" class="link-dark small">See all buying options</a>
|
||||
</div>
|
||||
</div>
|
||||
39
CatherineLynwood/Views/Discovery/_Layout.cshtml
Normal file
39
CatherineLynwood/Views/Discovery/_Layout.cshtml
Normal file
@ -0,0 +1,39 @@
|
||||
@{
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@section CSS{
|
||||
<style>
|
||||
.video-overlay {
|
||||
background-color: rgba(13, 202, 240, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
@RenderSection("Meta", required: false)
|
||||
}
|
||||
|
||||
|
||||
@section BackgroundVideo {
|
||||
<div id="background-wrapper">
|
||||
<div class="video-background">
|
||||
<video id="siteBackgroundVideo"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
preload="none">
|
||||
<!-- Source will be injected by JS -->
|
||||
</video>
|
||||
<div class="video-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@RenderBody()
|
||||
|
||||
@section Scripts {
|
||||
@RenderSection("Scripts", required: false)
|
||||
}
|
||||
@ -12,17 +12,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row align-items-center mb-5">
|
||||
<div class="row justify-content-center align-items-center mb-5">
|
||||
<div class="col-md-5 text-center">
|
||||
<a asp-controller="Discovery" asp-action="Index">
|
||||
<a asp-controller="Reckoning" asp-action="Index">
|
||||
<div class="hero-video-container">
|
||||
<video id="heroVideo" autoplay muted loop playsinline preload="none" poster="/images/webp/the-alpha-flame-discovery-blank-400.webp">
|
||||
<video id="heroVideo" autoplay muted loop playsinline preload="none" poster="/images/webp/the-alpha-flame-reckoning-400.webp">
|
||||
<!-- Source will be injected later -->
|
||||
</video>
|
||||
|
||||
<!-- Overlay image remains untouched -->
|
||||
<responsive-image src="the-alpha-flame-discovery-overlay.png"
|
||||
<responsive-image src="the-alpha-flame-reckoning-overlay.png"
|
||||
class="hero-overlay-image"
|
||||
alt="The Alpha Flame: Discovery by Catherine Lynwood"
|
||||
display-width-percentage="50"
|
||||
@ -33,34 +32,88 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-7 pt-2 pt-md-0">
|
||||
<h1 class="display-5 fw-bold">The Alpha Flame: <span class="fw-light">Discovery</span></h1>
|
||||
<p class="lead fst-italic tagline-shadow">Some girls survive. Others set the world on fire.</p>
|
||||
<p>
|
||||
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...
|
||||
|
||||
<h1 class="display-5 fw-bold">
|
||||
The Alpha Flame
|
||||
<span class="fw-light d-block fs-4">A Trilogy by Catherine Lynwood</span>
|
||||
</h1>
|
||||
|
||||
<p class="lead fst-italic tagline-shadow">
|
||||
Some girls survive. Others set the world on fire.
|
||||
</p>
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore the Book</a>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h2 class="h4 fw-bold">
|
||||
The Alpha Flame: <span class="fw-light">Discovery</span>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
The first novel introduces Maggie and Beth and the fragile, dangerous bond that forms between them.
|
||||
Set against 1980s Birmingham, <em>Discovery</em> is a story of survival, loyalty, and the moment a life quietly changes forever.
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap mb-4">
|
||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore Discovery</a>
|
||||
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
|
||||
</div>
|
||||
<p class="mt-4">
|
||||
<h2 class="h6 d-inline"><em>The Alpha Flame: Discovery</em></h2>, writen by <a asp-controller="Home" asp-action="AboutCatherineLynwood" class="link-dark fw-semibold">Catherine Lynwood</a>, 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 <strong>Reckoning</strong> (Spring 2026) and concludes with <strong>Redemption</strong> (Autumn 2026).
|
||||
Learn more about the full trilogy on the <a asp-controller="TheAlphaFlame" asp-action="Index" class="link-dark fw-semibold">Alpha Flame series page</a>.
|
||||
|
||||
<h2 class="h4 fw-bold">
|
||||
The Alpha Flame: <span class="fw-light">Reckoning</span>
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
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.
|
||||
<em>Reckoning</em> deepens the danger, raises the stakes, and proves that survival always comes at a cost.
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap mb-4">
|
||||
<a asp-controller="Reckoning" asp-action="Index" class="btn btn-dark">Discover Reckoning</a>
|
||||
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-dark">View the Full Trilogy</a>
|
||||
</div>
|
||||
|
||||
<p class="mt-4">
|
||||
<em>The Alpha Flame</em>, written by
|
||||
<a asp-controller="Home" asp-action="AboutCatherineLynwood" class="link-dark fw-semibold">Catherine Lynwood</a>,
|
||||
is a powerful trilogy of UK historical fiction following the intertwined lives of Maggie and Beth.
|
||||
The story begins with <strong>Discovery</strong>, intensifies in <strong>Reckoning</strong> (Spring 2026),
|
||||
and concludes with <strong>Redemption</strong> (Autumn 2026).
|
||||
</p>
|
||||
|
||||
@if (DateTime.Now < new DateTime(2025, 9, 1))
|
||||
{
|
||||
<p class="mt-4">
|
||||
<h2 class="display-6 fw-bold">Win: <span class="fw-light">a Collector’s Edition of The Alpha Flame: Discovery</span></h2>
|
||||
<em>Exclusive, limited, beautiful.</em>
|
||||
<hr class="my-4" />
|
||||
|
||||
<p>
|
||||
<h2 class="display-6 fw-bold">
|
||||
Win:
|
||||
<span class="fw-light">A Collector’s Edition of The Alpha Flame: Discovery</span>
|
||||
</h2>
|
||||
<em>Exclusive. Limited. Beautiful.</em>
|
||||
</p>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap mt-4">
|
||||
<a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">Enter now for your chance.</a>
|
||||
<a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">
|
||||
Enter now for your chance
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p class="mt-4">
|
||||
<h3 class="h6 d-inline"><em>About The Alpha Flame Trilogy</em></h3>, 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.
|
||||
<h3 class="h6 d-inline"><em>About the Trilogy</em></h3>
|
||||
Set in 1980s Birmingham, <em>The Alpha Flame</em> 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.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
157
CatherineLynwood/Views/Reckoning/Chapter1.cshtml
Normal file
157
CatherineLynwood/Views/Reckoning/Chapter1.cshtml
Normal file
@ -0,0 +1,157 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1";
|
||||
}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 1 - Drowning in Silence</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 1 - Drowning in Silence - Beth</h1>
|
||||
<p>An exclusive glimpse into Beth's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="beth-stood-in-bathroom.png" alt="Scene from Beth's story" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
|
||||
<div class="ratio ratio-16x9">
|
||||
<video controls="controls" poster="/images/Chapter-1.png" class="rounded-5">
|
||||
<source src="~/videos/Chapter-1-preview.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
</div>
|
||||
<p class="text-center text-muted small pt-2">
|
||||
Watch Beth narrating part of Chapter 1 - Drowning in Silence.
|
||||
</p>
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-1.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Beth narrating the complete Chapter 1 - Drowning in Silence.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">Drowning in Silence - Beth</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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.</em>
|
||||
</p>
|
||||
<p>
|
||||
<em>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...</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts{
|
||||
<script>
|
||||
const player = new Plyr('audio');
|
||||
</script>
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
<MetaTag meta-title="Chapter 1: Beth - The Alpha Flame by Catherine Lynwood"
|
||||
meta-description="Explore Chapter 1 of 'The Alpha Flame' by Catherine Lynwood. Discover Maggie's captivating story, full of determination and secrets, set in the vivid 1980s."
|
||||
meta-keywords="The Alpha Flame, Chapter 1, Maggie, Catherine Lynwood, 1980s fiction, family secrets, strong female characters, captivating novels, fiction by Catherine Lynwood"
|
||||
meta-author="Catherine Lynwood"
|
||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-1-beth"
|
||||
meta-image="https://www.catherinelynwood.com/images/webp/beth-stood-in-bathroom-600.webp"
|
||||
meta-image-alt="Beth from 'The Alpha Flame' by Catherine Lynwood"
|
||||
og-site-name="Catherine Lynwood - The Alpha Flame"
|
||||
article-published-time="@new DateTime(2024,11,20)"
|
||||
article-modified-time="@new DateTime(2024,11,20)"
|
||||
twitter-card-type="player"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood"
|
||||
twitter-player-width="480"
|
||||
twitter-player-height="80" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@@type": "Chapter",
|
||||
"name": "Chapter 1: Drowning in Silence – Beth",
|
||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-1-beth",
|
||||
"description": "Beth returns home to a haunting silence, discovering her mother lifeless in the bath. This moment shatters her world, leaving her feeling utterly alone.",
|
||||
"position": 1,
|
||||
"inLanguage": "en-GB",
|
||||
"isPartOf": {
|
||||
"@@type": "Book",
|
||||
"name": "The Alpha Flame: Discovery",
|
||||
"author": {
|
||||
"@@type": "Person",
|
||||
"name": "Catherine Lynwood",
|
||||
"url": "https://www.catherinelynwood.com"
|
||||
},
|
||||
"publisher": {
|
||||
"@@type": "Organization",
|
||||
"name": "Catherine Lynwood"
|
||||
},
|
||||
"inLanguage": "en-GB",
|
||||
"workExample": [
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Hardcover",
|
||||
"isbn": "978-1-0682258-0-2",
|
||||
"name": "The Alpha Flame: Discovery – Hardback"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Paperback",
|
||||
"isbn": "978-1-0682258-1-9",
|
||||
"name": "The Alpha Flame: Discovery – Softback"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Paperback",
|
||||
"isbn": "978-1-0682258-2-6",
|
||||
"name": "The Alpha Flame: Discovery – Amazon Edition"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/EBook",
|
||||
"isbn": "978-1-0682258-3-3",
|
||||
"name": "The Alpha Flame: Discovery – eBook"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
}
|
||||
173
CatherineLynwood/Views/Reckoning/Chapter13.cshtml
Normal file
173
CatherineLynwood/Views/Reckoning/Chapter13.cshtml
Normal file
@ -0,0 +1,173 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13";
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 13 - A Name She Never Owned</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 13 - A Name She Never Owned - Susie</h1>
|
||||
<p>An exclusive glimpse into Susie's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="pub-from-chapter-13.png" alt="The Pub from Chapter 13" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-13.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">A Name She Never Owned - Susie</p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Hi,” he said, smiling nervously. “I wondered if you’d remember.”</p>
|
||||
<p>“Of course,” I replied, returning his smile.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“These are for you.”</p>
|
||||
<p>“Wow! Thank you,” I said, genuinely blown away. “That’s so nice of you. No one’s ever bought me flowers before.”</p>
|
||||
<p><em>Ben’s smile deepened. “Are you ready?”</em></p>
|
||||
<p>“Sure.”</p>
|
||||
<p><em>He started the car and pulled away, driving slowly, a little nervously.</em></p>
|
||||
<p>“Have you been driving long?” I asked after a few moments.</p>
|
||||
<p><em>Ben glanced across. “About a year. Am I that bad?”</em></p>
|
||||
<p><em>I laughed softly. “No, not at all. It’s just that most guys seem to drive a lot faster.”</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>“Do you live on your own?”</p>
|
||||
<p>“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.”</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>“Thank you,” I said, feeling a blush creep up my cheeks.</p>
|
||||
<p>“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?”</p>
|
||||
<p><em>I smiled, warmed by his awkward honesty. “Suits me.”</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“This looks nice. Have you been here before?” I asked.</p>
|
||||
<p>“No, never. A friend told me about it when I asked him where we could go.”</p>
|
||||
<p><em>Shocked, I stared at him. “You told your mate you’re going on a date with me?”</em></p>
|
||||
<p>“Yeah. What’s wrong with that?”</p>
|
||||
<p>“Did you tell him I’m a prostitute?”</p>
|
||||
<p><em>Ben gave a half-smile. “I thought we agreed what each of us does doesn’t matter?”</em></p>
|
||||
<p>“It doesn’t.”</p>
|
||||
<p>“Exactly. But if you must know, I just told him I had a date with a beautiful girl. That’s all.”</p>
|
||||
<p>“So, you’re embarrassed to be seen with me?”</p>
|
||||
<p><em>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.”</em></p>
|
||||
<p><em>I was quite touched.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Do you want to go and sit down, and I’ll get the drinks?” Ben asked, glancing at the bar.</p>
|
||||
<p>“Yeah, okay. Can I have a vodka and Coke, please?”</p>
|
||||
<p><em>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.</em></p>
|
||||
<p><em>It wasn’t long before Ben returned with the drinks. He placed mine carefully in front of me.</em></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts{
|
||||
<script>
|
||||
const player = new Plyr('audio');
|
||||
</script>
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
<MetaTag meta-title="Chapter 2: Maggie - The Alpha Flame by Catherine Lynwood"
|
||||
meta-description="Explore Chapter 13 of 'The Alpha Flame' by Catherine Lynwood. Discover Susie's captivating story, full of determination and secrets, set in the vivid 1980s."
|
||||
meta-keywords="The Alpha Flame, Chapter 13, Susie, Catherine Lynwood, 1980s fiction, family secrets, strong female characters, captivating novels, fiction by Catherine Lynwood"
|
||||
meta-author="Catherine Lynwood"
|
||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-13-susie"
|
||||
meta-image="https://www.catherinelynwood.com/images/webp/pub-from-chapter-13-600.webp"
|
||||
meta-image-alt="Maggie from 'The Alpha Flame' by Catherine Lynwood"
|
||||
og-site-name="Catherine Lynwood - The Alpha Flame"
|
||||
article-published-time="@new DateTime(2024, 11, 20)"
|
||||
article-modified-time="@new DateTime(2024, 11, 20)"
|
||||
twitter-card-type="player"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood"
|
||||
twitter-player-width="480"
|
||||
twitter-player-height="80" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@@type": "Chapter",
|
||||
"name": "Chapter 13: A Name She Never Owned - Susie",
|
||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-13-susie",
|
||||
"description": "Maggie Grant bursts onto the page with wit, confidence, and a fiery spirit. As she faces challenges at college and flirts with independence, her strength and secrets begin to unfold.",
|
||||
"position": 13,
|
||||
"inLanguage": "en-GB",
|
||||
"isPartOf": {
|
||||
"@@type": "Book",
|
||||
"name": "The Alpha Flame: Discovery",
|
||||
"author": {
|
||||
"@@type": "Person",
|
||||
"name": "Catherine Lynwood",
|
||||
"url": "https://www.catherinelynwood.com"
|
||||
},
|
||||
"publisher": {
|
||||
"@@type": "Organization",
|
||||
"name": "Catherine Lynwood"
|
||||
},
|
||||
"inLanguage": "en-GB",
|
||||
"workExample": [
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Hardcover",
|
||||
"isbn": "978-1-0682258-0-2",
|
||||
"name": "The Alpha Flame: Discovery – Hardback"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Paperback",
|
||||
"isbn": "978-1-0682258-1-9",
|
||||
"name": "The Alpha Flame: Discovery – Softback"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Paperback",
|
||||
"isbn": "978-1-0682258-2-6",
|
||||
"name": "The Alpha Flame: Discovery – Amazon Edition"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/EBook",
|
||||
"isbn": "978-1-0682258-3-3",
|
||||
"name": "The Alpha Flame: Discovery – eBook"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
}
|
||||
156
CatherineLynwood/Views/Reckoning/Chapter2.cshtml
Normal file
156
CatherineLynwood/Views/Reckoning/Chapter2.cshtml
Normal file
@ -0,0 +1,156 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2";
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Chapter 2 - The Last Lesson</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">Chapter 2- The Last Lesson - Maggie</h1>
|
||||
<p>An exclusive glimpse into Maggie's story</p>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Scene Image -->
|
||||
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||
<responsive-image src="maggie-with-her-tr6-2.png" alt="Maggie With Her TR6" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-lg-7">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/the-alpha-flame-discovery-chapter-2.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<p class="text-center text-muted small">
|
||||
Listen to Maggie narrating the complete Chapter 2 - The Last Lesson,
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text">
|
||||
<p class="chapter-title">The Last Lesson - Maggie</p>
|
||||
<p><em>There was a knock at the door.</em></p>
|
||||
<p>“Colin’s here,” called mum.</p>
|
||||
<p>“Okay, I’ll be there in a second,” I replied.</p>
|
||||
<p><em>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,</em> I thought to myself, <em>trying to stay calm.</em></p>
|
||||
<p><em>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.</em> <em>Okay let’s do this</em>, I thought.</p>
|
||||
<p>“Maggie,” called Mum, “you’re going to be late.”</p>
|
||||
<p>“Coming.”</p>
|
||||
<p><em>I walked into the hallway where mum was talking to Colin.</em></p>
|
||||
<p>“How do I look?” I asked.</p>
|
||||
<p>Mum looked at me. “No bra?” she queried.</p>
|
||||
<p><em>I just looked at her, and mum smiled in response.</em></p>
|
||||
<p>Colin ran his eyes up and down my body. “Wow, you scrub up well,” he said.</p>
|
||||
<p>“Good luck, Sweetie,” said Mum.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Thank you,” I said. “What’s with the chivalry?”</p>
|
||||
<p>“No reason,” he replied, scurrying around to the passenger side and getting into the front seat.</p>
|
||||
<p><em>I started performing all my learner checks, seat belt, mirror, all that bosh, then started the car and put it into reverse.</em></p>
|
||||
<p>“Just take your time,” said Colin.</p>
|
||||
<p><em>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.</em></p>
|
||||
<p>“Jesus!” exclaimed Colin. “What are you doing?” as he stamped on the brake pedal.</p>
|
||||
<p>“Sorry, I’m rather nervous,” I said, looking over my shoulder at Mum, who was stood waving.</p>
|
||||
<p><em>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.</em> <em>Pull yourself together</em>, I thought.</p>
|
||||
<p>Calmly, he said, “Don’t worry. Let’s just try that again...”</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts{
|
||||
<script>
|
||||
const player = new Plyr('audio');
|
||||
</script>
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
<MetaTag meta-title="Chapter 2: Maggie - The Alpha Flame by Catherine Lynwood"
|
||||
meta-description="Explore Chapter 2 of 'The Alpha Flame' by Catherine Lynwood. Discover Maggie's captivating story, full of determination and secrets, set in the vivid 1980s."
|
||||
meta-keywords="The Alpha Flame, Chapter 2, Maggie, Catherine Lynwood, 1980s fiction, family secrets, strong female characters, captivating novels, fiction by Catherine Lynwood"
|
||||
meta-author="Catherine Lynwood"
|
||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-2-maggie"
|
||||
meta-image="https://www.catherinelynwood.com/images/webp/maggie-with-her-tr6-2-600.webp"
|
||||
meta-image-alt="Maggie from 'The Alpha Flame' by Catherine Lynwood"
|
||||
og-site-name="Catherine Lynwood - The Alpha Flame"
|
||||
article-published-time="@new DateTime(2024,11,20)"
|
||||
article-modified-time="@new DateTime(2024,11,20)"
|
||||
twitter-card-type="player"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood"
|
||||
twitter-player-width="480"
|
||||
twitter-player-height="80" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context": "https://schema.org",
|
||||
"@@type": "Chapter",
|
||||
"name": "Chapter 2: The Last Lesson – Maggie",
|
||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-2-maggie",
|
||||
"description": "Maggie Grant bursts onto the page with wit, confidence, and a fiery spirit. As she faces challenges at college and flirts with independence, her strength and secrets begin to unfold.",
|
||||
"position": 2,
|
||||
"inLanguage": "en-GB",
|
||||
"isPartOf": {
|
||||
"@@type": "Book",
|
||||
"name": "The Alpha Flame: Discovery",
|
||||
"author": {
|
||||
"@@type": "Person",
|
||||
"name": "Catherine Lynwood",
|
||||
"url": "https://www.catherinelynwood.com"
|
||||
},
|
||||
"publisher": {
|
||||
"@@type": "Organization",
|
||||
"name": "Catherine Lynwood"
|
||||
},
|
||||
"inLanguage": "en-GB",
|
||||
"workExample": [
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Hardcover",
|
||||
"isbn": "978-1-0682258-0-2",
|
||||
"name": "The Alpha Flame: Discovery – Hardback"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Paperback",
|
||||
"isbn": "978-1-0682258-1-9",
|
||||
"name": "The Alpha Flame: Discovery – Softback"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/Paperback",
|
||||
"isbn": "978-1-0682258-2-6",
|
||||
"name": "The Alpha Flame: Discovery – Amazon Edition"
|
||||
},
|
||||
{
|
||||
"@@type": "Book",
|
||||
"bookFormat": "https://schema.org/EBook",
|
||||
"isbn": "978-1-0682258-3-3",
|
||||
"name": "The Alpha Flame: Discovery – eBook"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
}
|
||||
160
CatherineLynwood/Views/Reckoning/Epilogue.cshtml
Normal file
160
CatherineLynwood/Views/Reckoning/Epilogue.cshtml
Normal file
@ -0,0 +1,160 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Epilogue";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Epilogue</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="fw-bold">The Alpha Flame: Discovery</h1>
|
||||
</div>
|
||||
|
||||
<!-- Excerpt Content -->
|
||||
<div class="row gx-5">
|
||||
<!-- Audio and Text -->
|
||||
<div class="col-12">
|
||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||
<responsive-image src="discovery-epilogue.png" class="card-img-top" alt="The Gang Having a Drink at The Barnt Green Inn" display-width-percentage="100"></responsive-image>
|
||||
|
||||
|
||||
<!-- Audio Player -->
|
||||
<div class="audio-player text-center">
|
||||
<audio controls>
|
||||
<source src="/audio/discovery-epilogue.mp3" type="audio/mpeg">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<div class="text-black text-center">
|
||||
<h2>Epilogue</h2>
|
||||
<h3>Narrated by Maggie – Late May 1983</h3>
|
||||
</div>
|
||||
<!-- Text Content -->
|
||||
<div class="chapter-text pt-3">
|
||||
<p><em>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.</em></p>
|
||||
<p><em>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.</em></p>
|
||||
<p>Rob spotted us and raised a hand. “Hey!”</p>
|
||||
<p>I leaned down and kissed him on the cheek. “Hiya. Can you get us a drink?”</p>
|
||||
<p>“Of course. What do you fancy?”</p>
|
||||
<p>“Coke for me. Beth?”</p>
|
||||
<p>She gave a small smile. “Cider, please.”</p>
|
||||
<p><em>Rob nodded and headed for the bar.</em></p>
|
||||
<p><em>Beth and I took the seats opposite Zoe.</em></p>
|
||||
<p>“How’ve you both been?” Zoe asked, giving us a once-over.</p>
|
||||
<p>I glanced at Beth. “Good… I think. Still getting used to it all.”</p>
|
||||
<p>Beth nodded. “Yeah. I’m alright. Put some weight on. I’m nearly as fat as Maggie now,” she grinned.</p>
|
||||
<p>I laughed, nudging her. “Nearly? That ship has sailed, love.”</p>
|
||||
<p>“Mum’s fault. Too many sausage sandwiches.”</p>
|
||||
<p>“You both look good,” Zoe said, eyes lingering just a little longer on Beth. “Bruises gone?”</p>
|
||||
<p><em>We nodded.</em></p>
|
||||
<p>“So, what have you two been doing with yourselves?”</p>
|
||||
<p>Beth perked up. “Helping Maggie with her designs. Turns out I’m not bad with a needle.”</p>
|
||||
<p>“And she’s not a bad model either,” I added. “Definitely helps having someone try things on.”</p>
|
||||
<p>Zoe smirked. “Do you make her twirl like they do on those fashion shows?”</p>
|
||||
<p>Beth put on a mock pout. “She makes me pose like I’m in Vogue. Arms here, head there.”</p>
|
||||
<p>“Only when you’re being annoying,” I said, laughing. “Besides, you love it.”</p>
|
||||
<p>“True,” Beth admitted. “It’s actually fun. I never thought I’d enjoy something like that.”</p>
|
||||
<p><em>Laughter echoed behind us as Rosie and Rebecca came round the corner.</em></p>
|
||||
<p>“She’s deadly,” Rebecca giggled. “I had my eyes closed the whole way here.”</p>
|
||||
<p>“It wasn’t that bad!” Rosie huffed, trying to look offended. “Not my fault people kept jumping out in front of me.”</p>
|
||||
<p>“You might try braking instead of shouting. It’s far more effective,” Rebecca teased.</p>
|
||||
<p>“Where’s Rob?” Rosie asked.</p>
|
||||
<p>“He’s at the bar. If you’re quick, you might get a free drink,” I said.</p>
|
||||
<p><em>Right on cue, Rob returned with a tray.</em></p>
|
||||
<p>He clocked the new arrivals and sighed. “I’m going back to the bar, aren’t I?”</p>
|
||||
<p>He placed the drinks down, then turned with theatrical weariness. “The usual, girls?”</p>
|
||||
<p>“Yes please,” said Rebecca sweetly.</p>
|
||||
<p><em>Once he was back and settled, we all relaxed into the rhythm of easy chatter.</em></p>
|
||||
<p>“So, who’s got summer plans then?” Rosie asked. “Please tell me someone’s going somewhere glamorous.”</p>
|
||||
<p>“Barnt Green counts as glamorous, right?” I said.</p>
|
||||
<p>“Only if you squint,” said Zoe. “And ignore the smell of wet dog that always seems to hang around the canal.”</p>
|
||||
<p>“I might take Beth to London,” I said. “Show her the big city.”</p>
|
||||
<p>Beth raised an eyebrow. “What for?”</p>
|
||||
<p>“So, you can be horrified at the price of everything and then come home saying how much nicer Birmingham is.”</p>
|
||||
<p>“Sold,” she laughed.</p>
|
||||
<p>Zoe leaned forward, lowering her voice slightly. “I heard from Graham. The case against Rick’s progressing.”</p>
|
||||
<p><em>Beth stiffened slightly but nodded.</em></p>
|
||||
<p>“Are you two going to be alright when it comes to court?”</p>
|
||||
<p><em>Beth and I exchanged a look. We’d talked about it endlessly in private.</em></p>
|
||||
<p>“We think so,” Beth said. “It’s black and white for us. We just have to tell the truth.”</p>
|
||||
<p><em>Rob had gone quiet, his brow furrowed.</em></p>
|
||||
<p>“You alright?” I asked.</p>
|
||||
<p>He hesitated, then said, “It’s just… I don’t believe in coincidences. Not ones like that.”</p>
|
||||
<p>Rebecca cocked her head. “Is he always this cryptic?”</p>
|
||||
<p>“Most of the time,” I said. “This one’s new, though. Go on, Rob. Spit it out.”</p>
|
||||
<p>“Rick,” he said. “The night Beth ended up under the flyover. Him just being there… it never sat right with me.”</p>
|
||||
<p>Beth’s face fell still. “You think he was following me?”</p>
|
||||
<p>Rob gave a half-shrug. “Feels too neat to be chance.”</p>
|
||||
<p>Beth looked down. “He did have the flat, though. Cindy lived there too, remember. Maybe he was nearby anyway.”</p>
|
||||
<p>“Maybe,” Rob said, but not like he believed it.</p>
|
||||
<p>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.”</p>
|
||||
<p>Beth’s eyes lit up. “I’ve never been to a grown-up party. Not really. I’m in.”</p>
|
||||
<p>“You’ll need something to wear,” I said. “And no, you’re not stealing that red skirt again.”</p>
|
||||
<p>“I only borrowed it.”</p>
|
||||
<p>“For three weeks.”</p>
|
||||
<p>Rosie smirked. “Are you two dressing the same again?”</p>
|
||||
<p>“Obviously,” we said in unison.</p>
|
||||
<p>Zoe groaned. “Have you noticed they do that constantly now?” She turned to Rob. “Seriously, get out while you still can.”</p>
|
||||
<p><em>Beth and I grinned at each other.</em></p>
|
||||
<p>“I really don’t know what they’re…”</p>
|
||||
<p>“…on about. Do you?” we said, still in sync.</p>
|
||||
<p>Rebecca had been quiet, but now her voice was cautious. “Are you going to look for your dad?”</p>
|
||||
<p><em>We both paused.</em></p>
|
||||
<p>“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.”</p>
|
||||
<p>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.”</p>
|
||||
<p><em>The table fell quiet for a moment.</em></p>
|
||||
<p>“Did your mum ever talk about him?” Rebecca asked.</p>
|
||||
<p>“Not really,” Beth said. “She used to say ‘He’s nobody worth knowing.’ But I’m not sure she meant it.”</p>
|
||||
<p>Rosie glanced up. “It’s such a lovely day. We should go to the beach one weekend.”</p>
|
||||
<p>Beth’s eyes went wide. “Yes! All of us. It’d be amazing.”</p>
|
||||
<p>“I’ll bring sandwiches,” said Rob. “Beth, you’re banned from cooking anything though.”</p>
|
||||
<p>“Hey! That cheese toastie was only slightly burnt.”</p>
|
||||
<p>“It was charcoal.”</p>
|
||||
<p>“I quite liked it,” I added. “Adds crunch.”</p>
|
||||
<p><em>General agreement followed.</em></p>
|
||||
<p>“I need the loo,” said Rebecca.</p>
|
||||
<p>“Me too. I’ll grab drinks on the way back. Same again?”</p>
|
||||
<p>“Perfect,” said Zoe.</p>
|
||||
<p>Once they were out of earshot, Zoe leaned in. “She really doesn’t know, does she? About Sophie.”</p>
|
||||
<p><em>We shook our heads.</em></p>
|
||||
<p>“They’re nothing alike,” I said. “Different planets.”</p>
|
||||
<p>“Sophie still avoiding you?” Zoe asked.</p>
|
||||
<p>I nodded. “Mostly. I saw her once. She didn’t stop.”</p>
|
||||
<p>Beth’s face darkened. “I can still see her face. That night. Just after she gave the order.”</p>
|
||||
<p>Zoe blinked. “You think she did?”</p>
|
||||
<p>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?”</p>
|
||||
<p>Zoe nodded slowly. “Graham thinks so too. Says something stinks, but he hasn’t nailed it yet.”</p>
|
||||
<p>Beth’s voice dropped to a whisper. “Mum wasn’t killed for nothing. Adam either. Rick even said so.”</p>
|
||||
<p>“Has Graham got anything else out of Rick?” I asked.</p>
|
||||
<p>“Nothing solid,” Zoe replied. “He’s clammed up. He’s gone ‘No comment’ on everything.”</p>
|
||||
<p>“And our aunt? Still scared?” Beth asked.</p>
|
||||
<p>“He’s working on her. She’s terrified. He’s got to go gently.”</p>
|
||||
<p>Beth’s voice hardened. “What about Baker?”</p>
|
||||
<p>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.”</p>
|
||||
<p><em>Rebecca and Rosie returned, laughing.</em></p>
|
||||
<p>“What have you two done now?” I asked.</p>
|
||||
<p>“She dropped a tray of drinks on some poor guy,” Rosie grinned. “She’s worse than Rob.”</p>
|
||||
<p>“Oi!” Rob protested. “That was once.”</p>
|
||||
<p>“Twice,” I said, grinning, and leaned in to kiss him.</p>
|
||||
<p>Beth rolled her eyes. “They’re going to be unbearable if they stay this happy.”</p>
|
||||
<p>“Jealous?” I teased.</p>
|
||||
<p>“A bit,” she admitted, then smiled. “But in the good way.”</p>
|
||||
<p><em>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.</em></p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
212
CatherineLynwood/Views/Reckoning/Extras.cshtml
Normal file
212
CatherineLynwood/Views/Reckoning/Extras.cshtml
Normal file
@ -0,0 +1,212 @@
|
||||
@{
|
||||
ViewData["Title"] = "Extras";
|
||||
|
||||
int? accessLevel = Context.Session.GetInt32("BookAccessLevel");
|
||||
int? accessBook = Context.Session.GetInt32("BookAccessMax");
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Extras</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-5">
|
||||
<h1 class="extras-header">Your Exclusive Extras</h1>
|
||||
|
||||
@if (accessBook == 1)
|
||||
{
|
||||
<div class="extras-grid mt-4">
|
||||
@if (accessLevel >= 1)
|
||||
{
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Epilogue</h5>
|
||||
<p class="card-text">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</p>
|
||||
<a asp-controller="Discovery" asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (accessLevel >= 2)
|
||||
{
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Discovery Scrap Book</h5>
|
||||
<p class="card-text">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.</p>
|
||||
<a asp-controller="Discovery" asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Discovery Soundtrack</h5>
|
||||
<p class="card-text">Have a listen to The Alpha Flame soundtrack. A selection of original songs written by me and put to music.</p>
|
||||
<a asp-controller="Discovery" asp-action="Soundtrack" class="btn btn-dark btn-sm">Soundtrack</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (accessLevel >= 3)
|
||||
{
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Listen to The Alpha Flame: Discovery</h5>
|
||||
<p class="card-text">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.</p>
|
||||
<a asp-controller="Discovery" asp-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Scrapbook: Maggie’s Designs</h5>
|
||||
<p class="card-text">Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.</p>
|
||||
<a asp-controller="Discovery" asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (accessBook == 2)
|
||||
{
|
||||
|
||||
<div class="extras-grid mt-4">
|
||||
@if (accessLevel >= 1)
|
||||
{
|
||||
|
||||
}
|
||||
else if (accessLevel >= 2)
|
||||
{
|
||||
|
||||
}
|
||||
else if (accessLevel >= 3)
|
||||
{
|
||||
|
||||
}
|
||||
else if(accessLevel >= 4)
|
||||
{
|
||||
|
||||
}
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Epilogue</h5>
|
||||
<p class="card-text">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</p>
|
||||
<a asp-controller="Discovery" asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Discovery Scrap Book</h5>
|
||||
<p class="card-text">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.</p>
|
||||
<a asp-controller="Discovery" asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Rubery Hill Photo Archive</h5>
|
||||
<p class="card-text">Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.</p>
|
||||
<a href="/extras/rubery-hill-photos" class="btn btn-dark btn-sm">Explore</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Scrapbook: Maggie’s Designs</h5>
|
||||
<p class="card-text">Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.</p>
|
||||
<a asp-controller="Discovery" asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
else if (accessBook == 3)
|
||||
{
|
||||
<div class="extras-grid mt-4">
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Epilogue</h5>
|
||||
<p class="card-text">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</p>
|
||||
<a asp-controller="Discovery" asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Discovery Scrap Book</h5>
|
||||
<p class="card-text">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.</p>
|
||||
<a asp-controller="Discovery" asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Rubery Hill Photo Archive</h5>
|
||||
<p class="card-text">Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.</p>
|
||||
<a href="/extras/rubery-hill-photos" class="btn btn-dark btn-sm">Explore</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card extra-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Scrapbook: Maggie’s Designs</h5>
|
||||
<p class="card-text">Flip through Maggie’s sketches, fashion notes, and photos from her original designs – including the infamous red skirt.</p>
|
||||
<a asp-controller="Discovery" asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@section Meta{
|
||||
<style>
|
||||
.extras-header {
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.extra-card {
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.extra-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.extras-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.access-label {
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
165
CatherineLynwood/Views/Reckoning/HowToBuy.cshtml
Normal file
165
CatherineLynwood/Views/Reckoning/HowToBuy.cshtml
Normal file
@ -0,0 +1,165 @@
|
||||
@{
|
||||
ViewData["Title"] = "How to Buy The Alpha Flame: Discovery";
|
||||
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">How to Buy</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4">How to Buy <span class="fw-light">The Alpha Flame: Discovery</span></h1>
|
||||
|
||||
<p class="lead">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 <strong>direct links</strong> below are the best way to do so.</p>
|
||||
|
||||
<!-- eBook -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fad fa-tablet-alt text-primary me-2"></i> eBook (Kindle)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>The Kindle edition is available via your local Amazon store:</p>
|
||||
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" class="btn btn-outline-dark mb-2" target="_blank">
|
||||
Buy Kindle Edition
|
||||
</a>
|
||||
<p class="small text-muted">Automatically redirects based on your country.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paperback -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fad fa-book text-success me-2"></i> Paperback (Bookshop Edition)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
This version is designed for local bookstores and global retailers.
|
||||
</p>
|
||||
<!-- IngramSpark direct paperback link placeholder -->
|
||||
<a id="paperbackLinkSelf" href="https://shop.ingramspark.com/b/084?params=6easpH54PaugzXFKdF4Tu4Izb0cvkMqbj3ZNlaYBKMJ" class="btn btn-outline-dark mb-2" target="_blank">
|
||||
📦 Buy Direct (Save & Support Author)
|
||||
</a>
|
||||
<a id="paperbackLink" href="https://www.amazon.com/dp/1068225815" class="btn btn-outline-dark mb-2" target="_blank">
|
||||
Buy on Amazon
|
||||
</a>
|
||||
<p class="small text-muted mb-0">ISBN 978-1-0682258-1-9</p>
|
||||
<p class="small text-muted" id="extraRetailers"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hardback -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fad fa-gem text-danger me-2"></i> Collector’s Edition (Hardback)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
A premium collector’s hardback edition, available via bookstores and online.
|
||||
</p>
|
||||
<!-- IngramSpark direct hardback link placeholder -->
|
||||
<a id="hardbackLinkSelf" href="https://shop.ingramspark.com/b/084?params=GC1p1c8b66Rhfoy6Tq97SJmmhdZSEYuxBcCY5zxNstO" class="btn btn-outline-dark mb-2" target="_blank">
|
||||
💎 Buy Direct (Save & Support Author)
|
||||
</a>
|
||||
<a id="hardbackLink" href="https://www.amazon.com/dp/1068225807" class="btn btn-outline-dark mb-2" target="_blank">
|
||||
Buy on Amazon
|
||||
</a>
|
||||
|
||||
<p class="small text-muted mb-0">ISBN 978-1-0682258-0-2</p>
|
||||
<p class="small text-muted" id="extraRetailersHardback"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audiobook -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fad fa-headphones-alt text-info me-2"></i> Audiobook (AI-Read)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2">Listen to the entire book for free on ElevenLabs (Elevenlabs subscription required):</p>
|
||||
<a href="https://elevenreader.io/audiobooks/the-alpha-flame/e4Ppi7wLTLGOLrWe3Y6q?voiceId=Xb7hH8MSUJpSbSDYk0k2" class="btn btn-outline-dark mb-3" target="_blank">
|
||||
🎧 Listen on ElevenLabs
|
||||
</a>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@section Scripts{
|
||||
<!-- Geo-based link adjustment -->
|
||||
<script>
|
||||
fetch('https://ipapi.co/json/')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const country = data.country_code;
|
||||
|
||||
let kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
|
||||
let paperbackLink = "https://www.amazon.com/dp/1068225815";
|
||||
let hardbackLink = "https://www.amazon.com/dp/1068225807";
|
||||
let extraRetailers = "";
|
||||
let extraRetailersHardback = "";
|
||||
|
||||
switch (country) {
|
||||
case "GB":
|
||||
kindleLink = "https://www.amazon.co.uk/dp/B0FBS427VD";
|
||||
paperbackLink = "https://www.amazon.co.uk/dp/1068225815";
|
||||
hardbackLink = "https://www.amazon.co.uk/dp/1068225807";
|
||||
extraRetailers = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819" target="_blank">Waterstones</a>';
|
||||
extraRetailersHardback = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225802" target="_blank">Waterstones</a>';
|
||||
break;
|
||||
case "US":
|
||||
kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
|
||||
paperbackLink = "https://www.amazon.com/dp/1068225815";
|
||||
hardbackLink = "https://www.amazon.com/dp/1068225807";
|
||||
extraRetailers = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225819" target="_blank">Barnes & Noble</a>';
|
||||
extraRetailersHardback = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225802" target="_blank">Barnes & Noble</a>';
|
||||
break;
|
||||
case "CA":
|
||||
kindleLink = "https://www.amazon.ca/dp/B0FBS427VD";
|
||||
paperbackLink = "https://www.amazon.ca/dp/1068225815";
|
||||
hardbackLink = "https://www.amazon.ca/dp/1068225807";
|
||||
break;
|
||||
case "AU":
|
||||
kindleLink = "https://www.amazon.com.au/dp/B0FBS427VD";
|
||||
paperbackLink = "https://www.amazon.com.au/dp/1068225815";
|
||||
hardbackLink = "https://www.amazon.com.au/dp/1068225807";
|
||||
break;
|
||||
}
|
||||
|
||||
// Set Amazon + retailer content
|
||||
const elKindle = document.getElementById("kindleLink");
|
||||
const elPbAmazon = document.getElementById("paperbackLink");
|
||||
const elHbAmazon = document.getElementById("hardbackLink");
|
||||
const elExtra = document.getElementById("extraRetailers");
|
||||
const elExtraHb = document.getElementById("extraRetailersHardback");
|
||||
|
||||
if (elKindle) elKindle.setAttribute("href", kindleLink);
|
||||
if (elPbAmazon) elPbAmazon.setAttribute("href", paperbackLink);
|
||||
if (elHbAmazon) elHbAmazon.setAttribute("href", hardbackLink);
|
||||
if (elExtra) elExtra.innerHTML = extraRetailers;
|
||||
if (elExtraHb) elExtraHb.innerHTML = extraRetailersHardback;
|
||||
|
||||
// Show IngramSpark only in GB/US; hide elsewhere
|
||||
const showIngram = country === "GB" || country === "US";
|
||||
["paperbackLinkSelf", "hardbackLinkSelf"].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.classList.toggle("d-none", !showIngram); // add when false, remove when true
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// If the geo lookup fails, leave links as-is
|
||||
});
|
||||
</script>
|
||||
|
||||
}
|
||||
|
||||
285
CatherineLynwood/Views/Reckoning/Index.cshtml
Normal file
285
CatherineLynwood/Views/Reckoning/Index.cshtml
Normal file
@ -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})";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Reckoning</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HERO: Cover + Coming Soon panel -->
|
||||
<section class="mb-4">
|
||||
<div class="row g-3 align-items-stretch">
|
||||
|
||||
<!-- Book Cover -->
|
||||
<div class="col-lg-5 d-flex">
|
||||
<div class="card character-card h-100 flex-fill" id="cover-card">
|
||||
<responsive-image src="the-alpha-flame-reckoning-cover.png"
|
||||
class="card-img-top"
|
||||
alt="The Alpha Flame: Reckoning book cover"
|
||||
display-width-percentage="50"></responsive-image>
|
||||
<div class="card-body border-top border-3 border-dark">
|
||||
<h3 class="card-title h5 mb-1">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
|
||||
<p class="card-text mb-0">
|
||||
Book 2 of the Alpha Flame trilogy.
|
||||
The danger didn’t end with <em>Discovery</em>. It got smarter.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Launch panel -->
|
||||
<div class="col-lg-7 d-flex">
|
||||
<div class="card character-card h-100 flex-fill" id="hero-media-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
|
||||
<div>
|
||||
|
||||
<h3 class="card-title">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
|
||||
|
||||
<p class="fst-italic">
|
||||
Two flames, once divided, now burn as one. Bound by blood, scarred by secrets, they rise to face the fire that made them.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
After everything they survived in <em>Discovery</em>, 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.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h4 class="mb-3"><i class="fa-solid fa-fire-flame-curved me-2"></i>The search for truth</h4>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-secondary mt-4" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Things escalate fast.</strong> A familiar car appears. Old fear becomes new danger. Running stops being a metaphor.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Call to action row -->
|
||||
<div class="d-flex gap-3 flex-wrap">
|
||||
@* <a asp-controller="TheAlphaFlame" asp-action="MailingList" class="btn btn-dark">
|
||||
Notify me on release
|
||||
</a> *@
|
||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-outline-dark">
|
||||
Start with Discovery
|
||||
</a>
|
||||
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-secondary">
|
||||
Explore the trilogy
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Small continuity note -->
|
||||
<div class="mt-3 small text-muted">
|
||||
<strong>Tip:</strong> <em>Reckoning</em> follows directly after <em>Discovery</em>.
|
||||
If you’re new to the series, start there for maximum emotional damage.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Synopsis (pre-release: shorter, tighter) -->
|
||||
<section id="synopsis" class="mb-4">
|
||||
<div class="card character-card text-white"
|
||||
style="background: url('/images/webp/bridge-over-the-river-960.webp'); background-position: center; background-size: cover;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title h1 mb-0">The Alpha Flame: <span class="fw-light">Reckoning</span></h2>
|
||||
<p class="mb-0">When the past returns, it doesn’t knock. It takes.</p>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<h4 class="mb-3"><i class="fa-solid fa-car-burst me-2"></i>The watcher returns</h4>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h4 class="mb-3"><i class="fad fa-mountain me-2"></i>Wales, family, and the key</h4>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h4 class="mb-3"><i class="fad fa-user-secret me-2"></i>Names that change everything</h4>
|
||||
|
||||
<p class="card-text">
|
||||
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: <strong>Simon Jones</strong>.
|
||||
Not a faceless monster, but a real figure with reach, money, and a network built on fear.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h4 class="mb-3">The boxes open</h4>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
Their mother did not leave them a neat answer.
|
||||
She left them a weapon, and a warning.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h4 class="mb-3"><i class="fad fa-heart me-2"></i>The reckoning hits home</h4>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<h4 class="mb-3"><i class="fad fa-door-open me-2"></i>The knock</h4>
|
||||
|
||||
<p class="card-text">
|
||||
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.
|
||||
</p>
|
||||
|
||||
|
||||
<div class="mt-3 d-flex gap-2 flex-wrap">
|
||||
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-light btn-sm">
|
||||
Unlock Discovery extras
|
||||
</a>
|
||||
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-outline-light btn-sm">
|
||||
The trilogy overview
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pre-release: No fake previews / no empty reviews -->
|
||||
@* <section class="mt-4">
|
||||
<div class="card character-card">
|
||||
<div class="card-body">
|
||||
<h2 class="h5 fw-bold mb-2">Preview content</h2>
|
||||
<p class="mb-3">
|
||||
Previews, sample chapters, and reader reviews will appear here once they’re ready.
|
||||
Until then, <em>Discovery</em> is available now.
|
||||
</p>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore Discovery</a>
|
||||
<a asp-controller="Discovery" asp-action="Reviews" class="btn btn-outline-dark">See Discovery reviews</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section> *@
|
||||
|
||||
@section Scripts {
|
||||
<!-- Plyr for audio (only needed if you keep the audio block) -->
|
||||
<script>
|
||||
const player = new Plyr('audio');
|
||||
</script>
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
<MetaTag meta-title="The Alpha Flame: Reckoning by Catherine Lynwood (Pre-Release)"
|
||||
meta-description="Reckoning is the upcoming second book in The Alpha Flame trilogy. 1983 Birmingham. Consequences, secrets, and survival… Coming 1 April 2026."
|
||||
meta-keywords="The Alpha Flame Reckoning, Catherine Lynwood, 1983 novel, Birmingham fiction, suspense fiction, family secrets, psychological crime, historical drama"
|
||||
meta-author="Catherine Lynwood"
|
||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/reckoning"
|
||||
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-reckoning-cover-1200.webp"
|
||||
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-reckoning-cover.png"
|
||||
meta-image-alt="The Alpha Flame: Reckoning by Catherine Lynwood"
|
||||
og-site-name="Catherine Lynwood - The Alpha Flame: Reckoning"
|
||||
article-published-time="@new DateTime(2026, 04, 01)"
|
||||
article-modified-time="@DateTime.UtcNow"
|
||||
twitter-card-type="summary_large_image"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood" />
|
||||
}
|
||||
674
CatherineLynwood/Views/Reckoning/Listen.cshtml
Normal file
674
CatherineLynwood/Views/Reckoning/Listen.cshtml
Normal file
@ -0,0 +1,674 @@
|
||||
@{
|
||||
ViewData["Title"] = "Listen to The Alpha Flame: Discovery";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Listen</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="h3 text-center" id="status">Loading...</p>
|
||||
<ul class="pagination justify-content-center" id="chapter-list"></ul>
|
||||
<audio id="audio" preload="none"></audio>
|
||||
<div class="input-group border border-2 border-dark rounded-3">
|
||||
<!-- Progress Bar (styled to look like an input) -->
|
||||
<div class="form-control p-0" style="height: 40px;">
|
||||
<div id="chapter-progress" class="progress rounded-end-0" style="height: 100%; margin: 0;">
|
||||
<div id="chapter-progress-bar" class="progress-bar rounded-end-0" role="progressbar" style="width: 0%"></div>
|
||||
<div id="chapter-ticks" class="d-none d-md-block"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Play / Pause Button -->
|
||||
<button id="toggle-play" class="btn btn-primary" style="height: 40px;">
|
||||
<i class="fad fa-pause"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1 mb-2">
|
||||
<div class="progress border border-2 border-dark rounded-3" style="height: 12px;">
|
||||
<div id="buffer-progress-bar" class="progress-bar bg-primary" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chapter-timestamp" class="my-2 small text-center"></div>
|
||||
|
||||
|
||||
|
||||
<div id="chapter-text-container" class="chapter-text-scroll">
|
||||
<div id="chapter-text-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
const audioBlobCache = new Map(); // id => Blob
|
||||
const audioBlobSizes = new Map(); // id => size in bytes
|
||||
let totalBlobSize = 0;
|
||||
const MAX_BLOB_CACHE_SIZE = 100 * 1024 * 1024; // 100 MB
|
||||
const BUFFER_TARGET_SECONDS = 30 * 60; // 30 minutes
|
||||
|
||||
let masterPlaylist = [];
|
||||
let chapterStartIndices = [];
|
||||
let currentIndex = 0;
|
||||
let preloadedAudio = null;
|
||||
|
||||
const audio = document.getElementById("audio");
|
||||
const status = document.getElementById("status");
|
||||
const toggleButton = document.getElementById("toggle-play");
|
||||
|
||||
// Load the playlist
|
||||
fetch("/api/audio/playlist/all")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// ✅ Sort chapters numerically by chapter number
|
||||
data.sort((a, b) => {
|
||||
const aNum = parseInt(a.chapter.match(/Chapter_(\d+)/i)?.[1] || "0", 10);
|
||||
const bNum = parseInt(b.chapter.match(/Chapter_(\d+)/i)?.[1] || "0", 10);
|
||||
return aNum - bNum;
|
||||
});
|
||||
|
||||
let flatIndex = 0;
|
||||
chapterStartIndices = []; // global or higher scoped variable
|
||||
masterPlaylist = []; // reset in case of reload
|
||||
|
||||
data.forEach(chapter => {
|
||||
chapterStartIndices.push({ index: flatIndex, name: chapter.chapter });
|
||||
|
||||
chapter.segments.forEach(segment => {
|
||||
masterPlaylist.push({
|
||||
id: segment.id,
|
||||
display: segment.display,
|
||||
chapter: chapter.chapter,
|
||||
duration: parseDuration(segment.display),
|
||||
text: segment.text || ""
|
||||
});
|
||||
flatIndex++;
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ Determine saved/resume position
|
||||
const savedIndex = parseInt(localStorage.getItem("lastAudioIndex") || "0", 10);
|
||||
const currentSeg = masterPlaylist[savedIndex] || {};
|
||||
const currentChapter = currentSeg.chapter || chapterStartIndices[0].name;
|
||||
|
||||
// ✅ Ensure pagination is rendered properly
|
||||
renderChapterPagination(currentChapter);
|
||||
|
||||
status.textContent = `Ready – ${masterPlaylist.length} segments total.`;
|
||||
|
||||
// ✅ Preload first segments of each chapter (for snappy chapter switches)
|
||||
preloadFirstSegments();
|
||||
|
||||
// ✅ Preload 10 minutes of sequential audio starting from resume position or beginning
|
||||
const preloadStart = (savedIndex >= 0 && savedIndex < masterPlaylist.length)
|
||||
? savedIndex
|
||||
: 0;
|
||||
|
||||
preloadSegmentsAhead(preloadStart, BUFFER_TARGET_SECONDS); // preload 10 mins ahead
|
||||
|
||||
|
||||
// ✅ Resume if index is valid
|
||||
if (savedIndex >= 0 && savedIndex < masterPlaylist.length) {
|
||||
const seg = masterPlaylist[savedIndex];
|
||||
renderChapterText(seg.chapter);
|
||||
initChapterProgress(seg.chapter);
|
||||
loadSegment(savedIndex);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
function fetchBlobWithRetry(url, retries = 3, delayMs = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const attempt = (n) => {
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.blob();
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(err => {
|
||||
if (n > 0) {
|
||||
setTimeout(() => attempt(n - 1), delayMs);
|
||||
} else {
|
||||
console.warn(`Failed to fetch blob ${url}:`, err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
attempt(retries);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderChapterPagination(currentChapter) {
|
||||
let listHtml = '';
|
||||
const currentPos = chapterStartIndices.findIndex(c => c.name === currentChapter);
|
||||
const totalChapters = chapterStartIndices.length;
|
||||
|
||||
// Dynamically determine window size based on screen width
|
||||
const windowSize = getWindowSize();
|
||||
const halfWindow = Math.floor(windowSize / 2);
|
||||
|
||||
// Previous button
|
||||
listHtml += `<li class="page-item ${currentPos <= 0 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="goToPrevChapter(${currentPos})"><i class="fad fa-step-backward"></i></a>
|
||||
</li>`;
|
||||
|
||||
const addPageItem = (i, label = null) => {
|
||||
const chapterNumber = chapterStartIndices[i].name.match(/Chapter_(\d+)/i)?.[1] || "?";
|
||||
const isActive = i === currentPos;
|
||||
listHtml += `<li class="page-item ${isActive ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="startFrom(${chapterStartIndices[i].index})">${label || chapterNumber}</a>
|
||||
</li>`;
|
||||
};
|
||||
|
||||
// First button
|
||||
addPageItem(0);
|
||||
|
||||
// Ellipsis after first if needed
|
||||
if (currentPos - halfWindow > 1) {
|
||||
listHtml += `<li class="page-item disabled d-none d-md-block"><span class="page-link">…</span></li>`;
|
||||
}
|
||||
|
||||
// Sliding window
|
||||
const start = Math.max(1, currentPos - halfWindow);
|
||||
const end = Math.min(totalChapters - 2, currentPos + halfWindow);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i !== 0 && i !== totalChapters - 1) {
|
||||
addPageItem(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Ellipsis before last if needed
|
||||
if (currentPos + halfWindow < totalChapters - 2) {
|
||||
listHtml += `<li class="page-item disabled d-none d-md-block"><span class="page-link">…</span></li>`;
|
||||
}
|
||||
|
||||
// Last button
|
||||
if (totalChapters > 1) {
|
||||
addPageItem(totalChapters - 1);
|
||||
}
|
||||
|
||||
// Next button
|
||||
listHtml += `<li class="page-item ${currentPos >= totalChapters - 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="goToNextChapter(${currentPos})"><i class="fad fa-step-forward"></i></a>
|
||||
</li>`;
|
||||
|
||||
document.getElementById("chapter-list").innerHTML = listHtml;
|
||||
}
|
||||
|
||||
function getWindowSize() {
|
||||
const width = window.innerWidth;
|
||||
|
||||
if (width < 576) {
|
||||
return 5; // Extra small (mobile)
|
||||
} else if (width < 768) {
|
||||
return 7; // Small (phones / small tablets)
|
||||
} else if (width < 992) {
|
||||
return 9; // Medium (tablets)
|
||||
} else if (width < 1200) {
|
||||
return 13; // Large (small desktops)
|
||||
} else {
|
||||
return 19; // Extra large (wide desktop screens)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goToPrevChapter(currentPos) {
|
||||
if (currentPos <= 0) return;
|
||||
const newIndex = chapterStartIndices[currentPos - 1].index;
|
||||
const newChapter = chapterStartIndices[currentPos - 1].name;
|
||||
renderChapterPagination(newChapter);
|
||||
startFrom(newIndex);
|
||||
}
|
||||
|
||||
function goToNextChapter(currentPos) {
|
||||
if (currentPos >= chapterStartIndices.length - 1) return;
|
||||
const newIndex = chapterStartIndices[currentPos + 1].index;
|
||||
const newChapter = chapterStartIndices[currentPos + 1].name;
|
||||
renderChapterPagination(newChapter);
|
||||
startFrom(newIndex);
|
||||
}
|
||||
|
||||
|
||||
function parseDuration(display) {
|
||||
const parts = display.split(":").map(Number);
|
||||
return parts[0] * 60 + (parts[1] || 0);
|
||||
}
|
||||
|
||||
// Call this in startFrom()
|
||||
function startFrom(index) {
|
||||
const seg = masterPlaylist[index];
|
||||
|
||||
// Ensure autoplay works (helps with Chrome mobile restrictions)
|
||||
audio.muted = false;
|
||||
audio.autoplay = true;
|
||||
|
||||
renderChapterPagination(seg.chapter);
|
||||
renderChapterText(seg.chapter);
|
||||
initChapterProgress(seg.chapter);
|
||||
loadSegment(index);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loadSegment(index) {
|
||||
if (index < 0 || index >= masterPlaylist.length) return;
|
||||
|
||||
currentIndex = index;
|
||||
updateBufferBar(index);
|
||||
|
||||
localStorage.setItem("lastAudioIndex", index.toString());
|
||||
const seg = masterPlaylist[index];
|
||||
|
||||
// ✅ Scroll and highlight current paragraph
|
||||
document.querySelectorAll(".chapter-text-scroll p").forEach(p => p.classList.remove("active"));
|
||||
const activePara = document.getElementById(`text-seg-${index}`);
|
||||
if (activePara) {
|
||||
activePara.classList.add("active");
|
||||
|
||||
const container = document.querySelector(".chapter-text-scroll");
|
||||
const paraTop = activePara.offsetTop;
|
||||
const paraHeight = activePara.offsetHeight;
|
||||
const containerHeight = container.clientHeight;
|
||||
const scrollTarget = paraTop - container.offsetTop - (containerHeight / 2) + (paraHeight / 2);
|
||||
container.scrollTo({ top: scrollTarget, behavior: "smooth" });
|
||||
}
|
||||
|
||||
const tryPlay = async (blob, retryCount = 0) => {
|
||||
try {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
audio.src = blobUrl;
|
||||
await audio.play();
|
||||
|
||||
status.textContent = `Playing: ${seg.chapter.replace(/_/g, ' ')}`;
|
||||
toggleButton.innerHTML = '<i class="fad fa-pause"></i>';
|
||||
} catch (err) {
|
||||
if (retryCount < 10) {
|
||||
console.warn(`Retrying audio playback (${retryCount + 1})...`);
|
||||
setTimeout(() => tryPlay(blob, retryCount + 1), 3000);
|
||||
} else {
|
||||
console.error("Playback failed permanently:", err);
|
||||
status.textContent = "Error loading audio segment.";
|
||||
toggleButton.innerHTML = '<i class="fad fa-play"></i>';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAndPlay = async () => {
|
||||
try {
|
||||
const response = await fetchWithRetry(`/api/audio/token-url/${seg.id}`, 5, 2000);
|
||||
const blob = await fetch(response).then(r => r.blob());
|
||||
|
||||
audioBlobCache.set(seg.id, blob);
|
||||
tryPlay(blob);
|
||||
} catch (err) {
|
||||
console.error("Initial fetch failed, will retry in background:", err);
|
||||
status.textContent = "Waiting for signal...";
|
||||
|
||||
let retries = 0;
|
||||
const maxRetries = 12;
|
||||
|
||||
const retryLoop = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetchWithRetry(`/api/audio/token-url/${seg.id}`, 3, 2000);
|
||||
const blob = await fetch(response).then(r => r.blob());
|
||||
|
||||
clearInterval(retryLoop);
|
||||
audioBlobCache.set(seg.id, blob);
|
||||
tryPlay(blob);
|
||||
} catch (err) {
|
||||
retries++;
|
||||
console.warn(`Retry ${retries}/${maxRetries} failed`);
|
||||
if (retries >= maxRetries) {
|
||||
clearInterval(retryLoop);
|
||||
status.textContent = "Unable to load segment.";
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
};
|
||||
|
||||
if (audioBlobCache.has(seg.id)) {
|
||||
tryPlay(audioBlobCache.get(seg.id));
|
||||
} else {
|
||||
fetchAndPlay();
|
||||
}
|
||||
|
||||
maybeTriggerPreload();
|
||||
|
||||
}
|
||||
|
||||
function maybeTriggerPreload() {
|
||||
if (audio.paused) return; // only preload while playing
|
||||
|
||||
const bufferedPercent = getBufferedPercent(currentIndex);
|
||||
if (bufferedPercent < 90) {
|
||||
preloadSegmentsAhead(currentIndex + 1, BUFFER_TARGET_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
function getBufferedPercent(currentIdx) {
|
||||
let bufferedSeconds = 0;
|
||||
let totalChecked = 0;
|
||||
let i = currentIdx + 1;
|
||||
|
||||
while (i < masterPlaylist.length && totalChecked < BUFFER_TARGET_SECONDS) {
|
||||
const seg = masterPlaylist[i];
|
||||
totalChecked += seg.duration;
|
||||
|
||||
if (audioBlobCache.has(seg.id)) {
|
||||
bufferedSeconds += seg.duration;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return Math.min((bufferedSeconds / BUFFER_TARGET_SECONDS) * 100, 100);
|
||||
}
|
||||
|
||||
|
||||
function preloadSegmentsAhead(startIndex, targetSeconds) {
|
||||
let totalDuration = 0;
|
||||
let i = startIndex;
|
||||
|
||||
while (i < masterPlaylist.length && totalDuration < targetSeconds) {
|
||||
const seg = masterPlaylist[i];
|
||||
|
||||
if (!audioBlobCache.has(seg.id)) {
|
||||
fetch(`/api/audio/token-url/${seg.id}`)
|
||||
.then(res => res.text())
|
||||
.then(url => fetchBlobWithRetry(url))
|
||||
.then(blob => {
|
||||
const blobSize = blob.size;
|
||||
audioBlobCache.set(seg.id, blob);
|
||||
audioBlobSizes.set(seg.id, blobSize);
|
||||
totalBlobSize += blobSize;
|
||||
enforceBlobCacheLimit();
|
||||
|
||||
updateBufferBar(currentIndex);
|
||||
|
||||
|
||||
})
|
||||
.catch(err => console.warn("Preload failed:", err));
|
||||
}
|
||||
|
||||
totalDuration += seg.duration;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fetchWithRetry(url, retries = 3, delayMs = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const attempt = (n) => {
|
||||
fetch(url)
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.text();
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(err => {
|
||||
if (n > 0) {
|
||||
setTimeout(() => attempt(n - 1), delayMs);
|
||||
} else {
|
||||
console.warn(`Failed to fetch ${url} after retries:`, err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
attempt(retries);
|
||||
});
|
||||
}
|
||||
|
||||
function renderChapterText(chapterName) {
|
||||
const container = document.getElementById("chapter-text-content");
|
||||
container.innerHTML = "";
|
||||
|
||||
masterPlaylist.forEach((seg, i) => {
|
||||
if (seg.chapter === chapterName) {
|
||||
const p = document.createElement("p");
|
||||
p.textContent = seg.text || "";
|
||||
p.id = `text-seg-${i}`;
|
||||
container.appendChild(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let currentChapter = "";
|
||||
let chapterSegments = [];
|
||||
let chapterTotalDuration = 0;
|
||||
|
||||
function initChapterProgress(chapterName) {
|
||||
currentChapter = chapterName;
|
||||
chapterSegments = masterPlaylist.filter(s => s.chapter === chapterName);
|
||||
chapterTotalDuration = chapterSegments.reduce((sum, seg) => sum + seg.duration, 0);
|
||||
|
||||
updateProgressUI(0);
|
||||
renderChapterTicks(); // ✅ Added here
|
||||
}
|
||||
|
||||
|
||||
function updateProgressUI(elapsedSeconds) {
|
||||
const percent = (elapsedSeconds / chapterTotalDuration) * 100;
|
||||
document.getElementById("chapter-progress-bar").style.width = `${percent}%`;
|
||||
|
||||
const mins = Math.floor(elapsedSeconds / 60).toString().padStart(2, "0");
|
||||
const secs = Math.floor(elapsedSeconds % 60).toString().padStart(2, "0");
|
||||
const totalMins = Math.floor(chapterTotalDuration / 60).toString().padStart(2, "0");
|
||||
const totalSecs = Math.floor(chapterTotalDuration % 60).toString().padStart(2, "0");
|
||||
|
||||
document.getElementById("chapter-timestamp").textContent = `${mins}:${secs} / ${totalMins}:${totalSecs}`;
|
||||
}
|
||||
|
||||
function getElapsedInChapter(currentIdx, audioCurrentTime) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < currentIdx; i++) {
|
||||
if (masterPlaylist[i].chapter === currentChapter) {
|
||||
sum += masterPlaylist[i].duration;
|
||||
}
|
||||
}
|
||||
return sum + audioCurrentTime;
|
||||
}
|
||||
|
||||
|
||||
function renderChapterTicks() {
|
||||
const ticksContainer = document.getElementById("chapter-ticks");
|
||||
ticksContainer.innerHTML = "";
|
||||
|
||||
let elapsed = 0;
|
||||
|
||||
chapterSegments.forEach(seg => {
|
||||
const percent = (elapsed / chapterTotalDuration) * 100;
|
||||
|
||||
const tick = document.createElement("div");
|
||||
tick.className = "chapter-tick";
|
||||
tick.style.left = `${percent}%`;
|
||||
ticksContainer.appendChild(tick);
|
||||
|
||||
// const label = document.createElement("div");
|
||||
// label.className = "chapter-tick-label";
|
||||
// label.style.left = `${percent}%`;
|
||||
// label.textContent = seg.display;
|
||||
|
||||
// ticksContainer.appendChild(label);
|
||||
elapsed += seg.duration;
|
||||
});
|
||||
}
|
||||
|
||||
function updatePaginationHighlight(currentChapter) {
|
||||
document.querySelectorAll("#chapter-list .page-item").forEach((li, idx) => {
|
||||
const chapterNum = parseInt(li.textContent);
|
||||
const currentNum = parseInt(currentChapter.match(/Chapter_(\d+)/i)?.[1]);
|
||||
|
||||
if (chapterNum === currentNum) {
|
||||
li.classList.add("active");
|
||||
} else {
|
||||
li.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function preloadFirstSegments() {
|
||||
chapterStartIndices.forEach(({ index }) => {
|
||||
const seg = masterPlaylist[index];
|
||||
if (!audioBlobCache.has(seg.id)) {
|
||||
fetch(`/api/audio/token-url/${seg.id}`)
|
||||
.then(res => res.text())
|
||||
.then(url => fetchBlobWithRetry(url))
|
||||
.then(blob => {
|
||||
const blobSize = blob.size;
|
||||
audioBlobCache.set(seg.id, blob);
|
||||
audioBlobSizes.set(seg.id, blobSize);
|
||||
totalBlobSize += blobSize;
|
||||
enforceBlobCacheLimit();
|
||||
|
||||
updateBufferBar(currentIndex);
|
||||
|
||||
})
|
||||
.catch(err => console.warn(`Preload of chapter start failed (${seg.id}):`, err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function enforceBlobCacheLimit() {
|
||||
const iterator = audioBlobCache.keys();
|
||||
|
||||
while (totalBlobSize > MAX_BLOB_CACHE_SIZE && audioBlobCache.size > 0) {
|
||||
const oldestKey = iterator.next().value;
|
||||
const size = audioBlobSizes.get(oldestKey) || 0;
|
||||
|
||||
audioBlobCache.delete(oldestKey);
|
||||
audioBlobSizes.delete(oldestKey);
|
||||
totalBlobSize -= size;
|
||||
|
||||
console.log(`Evicted ${oldestKey} to maintain cache size. Remaining size: ${totalBlobSize} bytes.`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBufferBar(currentIdx) {
|
||||
let bufferedSeconds = 0;
|
||||
let totalChecked = 0;
|
||||
let i = currentIdx + 1;
|
||||
|
||||
while (i < masterPlaylist.length && totalChecked < BUFFER_TARGET_SECONDS) {
|
||||
const seg = masterPlaylist[i];
|
||||
totalChecked += seg.duration;
|
||||
|
||||
if (audioBlobCache.has(seg.id)) {
|
||||
bufferedSeconds += seg.duration;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
const percent = Math.min((bufferedSeconds / BUFFER_TARGET_SECONDS) * 100, 100);
|
||||
const bar = document.getElementById("buffer-progress-bar");
|
||||
|
||||
bar.style.width = `${percent.toFixed(1)}%`;
|
||||
bar.setAttribute("aria-valuenow", percent.toFixed(1));
|
||||
|
||||
// ✅ Set Bootstrap colour classes
|
||||
bar.classList.remove("bg-primary", "bg-warning", "bg-danger");
|
||||
|
||||
if (percent >= 75) {
|
||||
bar.classList.add("bg-primary");
|
||||
} else if (percent >= 30) {
|
||||
bar.classList.add("bg-warning");
|
||||
} else {
|
||||
bar.classList.add("bg-danger");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
audio.addEventListener("ended", () => {
|
||||
if (currentIndex < masterPlaylist.length - 1) {
|
||||
const nextIndex = currentIndex + 1;
|
||||
const nextSeg = masterPlaylist[nextIndex];
|
||||
|
||||
const currentChapter = masterPlaylist[currentIndex].chapter;
|
||||
const nextChapter = nextSeg.chapter;
|
||||
|
||||
if (nextChapter !== currentChapter) {
|
||||
renderChapterPagination(nextChapter); // ✅ Update pagination
|
||||
renderChapterText(nextChapter);
|
||||
initChapterProgress(nextChapter);
|
||||
}
|
||||
|
||||
loadSegment(nextIndex);
|
||||
} else {
|
||||
status.textContent = "Playback complete.";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update during playback
|
||||
audio.addEventListener("timeupdate", () => {
|
||||
if (!chapterSegments.length || !audio.duration) return;
|
||||
|
||||
const elapsed = getElapsedInChapter(currentIndex, audio.currentTime);
|
||||
updateProgressUI(elapsed);
|
||||
});
|
||||
|
||||
toggleButton.addEventListener("click", () => {
|
||||
if (audio.paused) {
|
||||
audio.play();
|
||||
toggleButton.innerHTML = '<i class="fad fa-pause"></i>';
|
||||
} else {
|
||||
audio.pause();
|
||||
toggleButton.innerHTML = '<i class="fad fa-play"></i>';
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener("play", () => {
|
||||
toggleButton.innerHTML = '<i class="fad fa-pause"></i>';
|
||||
});
|
||||
|
||||
audio.addEventListener("pause", () => {
|
||||
toggleButton.innerHTML = '<i class="fad fa-play"></i>';
|
||||
});
|
||||
|
||||
// ✅ These are additional listeners — do not replace existing ones
|
||||
audio.addEventListener("play", maybeTriggerPreload);
|
||||
audio.addEventListener("ended", maybeTriggerPreload);
|
||||
|
||||
|
||||
document.getElementById("chapter-progress").addEventListener("click", (e) => {
|
||||
const container = e.currentTarget;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const clickPercent = clickX / rect.width;
|
||||
|
||||
const targetTime = clickPercent * chapterTotalDuration;
|
||||
|
||||
// Find approximate segment
|
||||
let cumulative = 0;
|
||||
for (let i = 0; i < chapterSegments.length; i++) {
|
||||
const seg = chapterSegments[i];
|
||||
if (cumulative + seg.duration >= targetTime) {
|
||||
const globalIndex = masterPlaylist.findIndex(s => s.id === seg.id);
|
||||
if (globalIndex >= 0) loadSegment(globalIndex);
|
||||
break;
|
||||
}
|
||||
cumulative += seg.duration;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
}
|
||||
375
CatherineLynwood/Views/Reckoning/MaggiesDesigns.cshtml
Normal file
375
CatherineLynwood/Views/Reckoning/MaggiesDesigns.cshtml
Normal file
@ -0,0 +1,375 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Maggie's Designs";
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Maggie's Designs</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1 class="mb-5 text-center">Maggie's Designs</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
That said, no matter what the outfit was I found working from a visual prompt enable me to write more vividly about it.
|
||||
</p>
|
||||
|
||||
<!-- Section 1 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="maggie-fashion-6.png" class="img-fluid" alt="The Elle Dress" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She didn’t dress to impress. She dressed to become, a force wrapped in colour, stitched in self-belief.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“The Elle Dress” – Fully Formed</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="maggie-fashion-12.png" class="img-fluid" alt="Forest Siren" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She didn’t rise from ashes. She bloomed from the soil, raw, rooted, and wild beyond taming.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Forest Siren” – The Evolution of Elle</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-3">
|
||||
<responsive-image src="maggie-fashion-3.png" class="img-fluid" alt="Ivory Fire" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She didn’t wear it to be wanted. She wore it to be undeniable.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Ivory Fire” – The One That Smouldered Off-Page</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-4">
|
||||
<responsive-image src="maggie-fashion-4.png" class="img-fluid" alt="Ember Veins" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“It burned beneath the fabric, not just colour, but something molten and unspoken. She was the match and the flame.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Ember Veins” – The Prototype</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="maggie-fashion-5.png" class="img-fluid" alt="Midnight Teal" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“It wasn’t about seduction. It was about control, about saying, ‘You can look… but you don’t get to touch.’”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Midnight Teal” – The Dress Before Elle</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="maggie-fashion-7.png" class="img-fluid" alt="Sleek Intentions" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“I can be soft. But don’t mistake me for breakable.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Sleek Intentions” – The Outfit That Almost Was</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>*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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 1 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="maggie-fashion-8.png" class="img-fluid" alt="Where the Poppies Burn" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She didn’t escape the fire. She walked through it, and the flowers grew behind her.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Where the Poppies Burn” – A Dream of Freedom</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="maggie-fashion-9.png" class="img-fluid" alt="Verdant Fury" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“You don’t rise from fire without learning how to burn.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Verdant Fury” – The Goddess Unleashed</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-3">
|
||||
<responsive-image src="maggie-fashion-10.png" class="img-fluid" alt="Unveiling Desires" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She wore it like a dare, and Zoe answered with a kiss.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Unveiling Desires” – The Painted Flame Jumpsuit</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-4">
|
||||
<responsive-image src="maggie-fashion-11.png" class="img-fluid" alt="Whispers in Lace" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She didn’t wear it… but for a moment, she almost did.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Whispers in Lace” – The Dress That Was Too Much</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="maggie-fashion-2.png" class="img-fluid" alt="Lace & Lager" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She wore hope in the shape of a dress. And for a moment, she almost believed it would be enough.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Lace & Lager” – The First Date Dress</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="maggie-fashion-13.png" class="img-fluid" alt="Velvet Resolve" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“It made her feel powerful… but not like herself.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Velvet Resolve” – The One That Stayed Behind</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="maggie-fashion.png" class="img-fluid" alt="Threadbare Ghost" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She didn’t need a scene to wear this. Just a window, and a reason to breathe.”
|
||||
</blockquote>
|
||||
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Threadbare Ghosts” – The Dress Without a Scene</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="maggie-fashion-14.png" class="img-fluid" alt="Summer Soft" display-width-percentage="50"></responsive-image>
|
||||
<blockquote class="text-black" style="font-style: italic; margin-top: 1em;">
|
||||
“She wore it first. Beth made it hers. But what stitched them together was never fabric.”
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>“Summer Soft” – The Outfit They Shared</h5>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@section Meta {
|
||||
<style>
|
||||
.scrapbook-section {
|
||||
margin-bottom: 4rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scrapbook-image {
|
||||
background: white;
|
||||
padding: 10px 10px 30px 10px;
|
||||
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 1rem;
|
||||
transform: rotate(var(--angle));
|
||||
}
|
||||
|
||||
.scrapbook-text {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.rotate-1 {
|
||||
--angle: -3deg;
|
||||
}
|
||||
|
||||
.rotate-2 {
|
||||
--angle: 2deg;
|
||||
}
|
||||
|
||||
.rotate-3 {
|
||||
--angle: -2deg;
|
||||
}
|
||||
|
||||
.rotate-4 {
|
||||
--angle: 3deg;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.scrapbook-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scrapbook-image {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
101
CatherineLynwood/Views/Reckoning/Reviews.cshtml
Normal file
101
CatherineLynwood/Views/Reckoning/Reviews.cshtml
Normal file
@ -0,0 +1,101 @@
|
||||
@model CatherineLynwood.Models.Reviews
|
||||
@{
|
||||
ViewData["Title"] = "Reader Reviews – The Alpha Flame: Discovery";
|
||||
}
|
||||
|
||||
<div class="container my-5">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Reviews</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<section class="mb-5 text-center">
|
||||
<h1 class="display-5 fw-bold">Reader Reviews</h1>
|
||||
<p class="lead">Here’s what readers are saying about <em>The Alpha Flame: Discovery</em>. If you’ve read the book, we’d love for you to share your thoughts too.</p>
|
||||
</section>
|
||||
|
||||
<section class="reader-reviews">
|
||||
@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");
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<blockquote class="blockquote ms-3 me-3 mt-3">
|
||||
<span class="mb-2 text-warning">
|
||||
@for (int i = 0; i < fullStars; i++)
|
||||
{
|
||||
<i class="fad fa-star"></i>
|
||||
}
|
||||
@if (hasHalfStar)
|
||||
{
|
||||
<i class="fad fa-star-half-alt"></i>
|
||||
}
|
||||
@for (int i = 0; i < emptyStars; i++)
|
||||
{
|
||||
<i class="fad fa-star" style="--fa-primary-opacity: 0.2; --fa-secondary-opacity: 0.2;"></i>
|
||||
}
|
||||
</span>
|
||||
@Html.Raw(review.ReviewBody)
|
||||
<footer class="blockquote-footer mt-2">
|
||||
@review.AuthorName on
|
||||
<cite title="@review.SiteName">
|
||||
@if (string.IsNullOrEmpty(review.URL))
|
||||
{
|
||||
@review.SiteName
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="@review.URL" target="_blank" rel="noopener">@review.SiteName</a>
|
||||
}
|
||||
</cite> — <span class="text-muted small">@reviewDate</span>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted text-center">There are no reviews to display yet. Be the first to leave one!</p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Meta{
|
||||
<MetaTag meta-title="The Alpha Flame: Discovery by Catherine Lynwood"
|
||||
meta-description="A gritty 1980s Birmingham crime novel about twin sisters uncovering dark family secrets and surviving abuse. Realistic, powerful, and unflinching — discover The Alpha Flame today."
|
||||
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
|
||||
meta-author="Catherine Lynwood"
|
||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp"
|
||||
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
|
||||
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
|
||||
article-published-time="@new DateTime(2024, 11, 20)"
|
||||
article-modified-time="@new DateTime(2025, 06, 07)"
|
||||
twitter-card-type="summary_large_image"
|
||||
twitter-site-handle="@@CathLynwood"
|
||||
twitter-creator-handle="@@CathLynwood" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
@Html.Raw(Model.SchemaJsonLd)
|
||||
</script>
|
||||
|
||||
}
|
||||
224
CatherineLynwood/Views/Reckoning/ScrapBook.cshtml
Normal file
224
CatherineLynwood/Views/Reckoning/ScrapBook.cshtml
Normal file
@ -0,0 +1,224 @@
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame: Discovery Scrap Book";
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Discovery Scrap Book</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1 class="mb-5 text-center">Discover Scrapbook</h1>
|
||||
<p>
|
||||
On this page I've included some of the images that helped inspire me to write The Alpha Flame: Discovery.
|
||||
</p>
|
||||
<p>
|
||||
Some of them are fictional, generated by AI, others are real, both recent and archive photos.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<!-- Section 1 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="flyover-at-night.png" class="img-fluid" alt="The Rubery flyover at night time" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>The Rubery Flyover</h5>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="beth-in-her-flat.png" class="img-fluid" alt="Beth in her flat" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>Beth and Her Flat</h5>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-3">
|
||||
<responsive-image src="rubery-hill-hospital.png" class="img-fluid" alt="Rubery Hill Hospital" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>Rubery Hill Hospital</h5>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-4">
|
||||
<div class="hero-video-container">
|
||||
<video autoplay muted loop playsinline poster="/images/maggie-in-her-jumpsuit.png">
|
||||
<source src="/videos/maggie-in-her-jumpsuit.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>Maggie in Her Jumpsuit</h5>
|
||||
<p>
|
||||
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!
|
||||
</p>
|
||||
<p>
|
||||
But wouldn't you just love to wear it!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-1">
|
||||
<responsive-image src="ricardos.png" class="img-fluid" alt="Ricardos" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>Ricardo's</h5>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 6 -->
|
||||
<div class="scrapbook-section flex-row-reverse">
|
||||
<div class="scrapbook-image rotate-2">
|
||||
<responsive-image src="beth-in-her-flat.png" class="img-fluid" alt="Beth in her flat" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>Beth and Her Flat</h5>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 7 -->
|
||||
<div class="scrapbook-section">
|
||||
<div class="scrapbook-image rotate-3">
|
||||
<responsive-image src="rubery-hill-hospital.png" class="img-fluid" alt="Rubery Hill Hospital" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="scrapbook-text">
|
||||
<h5>Rubery Hill Hospital</h5>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@section Meta {
|
||||
<style>
|
||||
.scrapbook-section {
|
||||
margin-bottom: 4rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scrapbook-image {
|
||||
background: white;
|
||||
padding: 10px 10px 30px 10px;
|
||||
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 1rem;
|
||||
transform: rotate(var(--angle));
|
||||
}
|
||||
|
||||
.scrapbook-text {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.rotate-1 {
|
||||
--angle: -3deg;
|
||||
}
|
||||
|
||||
.rotate-2 {
|
||||
--angle: 2deg;
|
||||
}
|
||||
|
||||
.rotate-3 {
|
||||
--angle: -2deg;
|
||||
}
|
||||
|
||||
.rotate-4 {
|
||||
--angle: 3deg;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.scrapbook-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scrapbook-image {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
198
CatherineLynwood/Views/Reckoning/Soundtrack.cshtml
Normal file
198
CatherineLynwood/Views/Reckoning/Soundtrack.cshtml
Normal file
@ -0,0 +1,198 @@
|
||||
@model List<CatherineLynwood.Models.SoundtrackTrackModel>
|
||||
@{
|
||||
ViewData["Title"] = "Alpha Flame • Soundtrack";
|
||||
}
|
||||
|
||||
<section class="container my-4" id="soundtrack">
|
||||
<header class="mb-4">
|
||||
<h1 class="h2">The Alpha Flame • Soundtrack</h1>
|
||||
<p class="text-muted mb-0">Eight original tracks inspired by key chapters; listen while you read…</p>
|
||||
</header>
|
||||
|
||||
<div class="row gy-4">
|
||||
@if (Model != null && Model.Any())
|
||||
{
|
||||
var index = 0;
|
||||
foreach (var track in Model)
|
||||
{
|
||||
var id = $"track-{index++}";
|
||||
<div class="col-12">
|
||||
<article class="card shadow-sm h-100">
|
||||
<div class="row g-0 align-items-stretch">
|
||||
<!-- Image + Play/Pause -->
|
||||
<div class="col-12 col-md-5 col-lg-3">
|
||||
<div class="position-relative h-100">
|
||||
<responsive-image src="@track.ImageUrl" class="img-fluid w-100 h-100 object-fit-cover rounded-start" alt="@track.Title image" display-width-percentage="50"></responsive-image>
|
||||
<button type="button"
|
||||
class="btn btn-light btn-lg rounded-circle position-absolute top-50 start-50 translate-middle track-toggle"
|
||||
aria-label="Play @track.Title"
|
||||
data-audio-id="@id">
|
||||
<i class="fad fa-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text -->
|
||||
<div class="col-12 col-md-7 col-lg-9">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h2 class="h4 mb-2">@track.Title</h2>
|
||||
@if (!string.IsNullOrWhiteSpace(track.Chapter) || !string.IsNullOrWhiteSpace(track.Description))
|
||||
{
|
||||
<p class="text-muted small mb-3">
|
||||
@if (!string.IsNullOrWhiteSpace(track.Chapter))
|
||||
{
|
||||
<span><strong>Chapter:</strong> @track.Chapter</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(track.Chapter) && !string.IsNullOrWhiteSpace(track.Description))
|
||||
{
|
||||
<span> • </span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(track.Description))
|
||||
{
|
||||
<span>@track.Description</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="lyrics border rounded p-3 mb-3 overflow-auto"
|
||||
style="max-height: 300px;">
|
||||
@if (!string.IsNullOrWhiteSpace(track.LyricsHtml))
|
||||
{
|
||||
@Html.Raw(track.LyricsHtml)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<button type="button"
|
||||
class="btn btn-outline-dark me-2 track-toggle"
|
||||
aria-label="Play @track.Title"
|
||||
data-audio-id="@id">
|
||||
<i class="fad fa-play me-1"></i> <span>Play</span>
|
||||
</button>
|
||||
<span class="text-muted small" data-duration-for="@id"></span>
|
||||
</div>
|
||||
|
||||
<!-- Hidden audio element -->
|
||||
<audio id="@id"
|
||||
preload="metadata"
|
||||
src="\audio\soundtrack\@track.AudioUrl"
|
||||
data-title="@track.Title"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
Tracks will appear here soon.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<div class="alert alert-warning mt-4">Enable JavaScript to play the soundtrack.</div>
|
||||
</noscript>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Keep images nicely cropped */
|
||||
.object-fit-cover { object-fit: cover; }
|
||||
|
||||
/* Make the overlay button stand out on varied artwork */
|
||||
.track-toggle.btn-light {
|
||||
--bs-btn-bg: rgba(255,255,255,.9);
|
||||
--bs-btn-border-color: rgba(0,0,0,.05);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15);
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.track-toggle .fa-play, .track-toggle .fa-pause { font-size: 1.25rem; }
|
||||
</style>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(function () {
|
||||
const cards = document.querySelectorAll('#soundtrack article.card');
|
||||
const toggles = document.querySelectorAll('.track-toggle');
|
||||
const audios = Array.from(document.querySelectorAll('#soundtrack audio'));
|
||||
|
||||
function setAllToStopped(exceptId) {
|
||||
audios.forEach(a => {
|
||||
if (a.id !== exceptId) {
|
||||
a.pause();
|
||||
a.currentTime = a.currentTime; // stop updating without resetting
|
||||
updateUI(a, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
const s = Math.round(seconds);
|
||||
const m = Math.floor(s / 60);
|
||||
const r = s % 60;
|
||||
return `${m}:${r.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function updateUI(audio, isPlaying) {
|
||||
// Update both toggles for this track
|
||||
const buttons = document.querySelectorAll(`.track-toggle[data-audio-id="${audio.id}"]`);
|
||||
buttons.forEach(btn => {
|
||||
const icon = btn.querySelector('i');
|
||||
const labelSpan = btn.querySelector('span');
|
||||
if (isPlaying) {
|
||||
btn.setAttribute('aria-label', `Pause ${audio.dataset.title}`);
|
||||
if (icon) { icon.classList.remove('fa-play'); icon.classList.add('fa-pause'); }
|
||||
if (labelSpan) { labelSpan.textContent = 'Pause'; }
|
||||
} else {
|
||||
btn.setAttribute('aria-label', `Play ${audio.dataset.title}`);
|
||||
if (icon) { icon.classList.remove('fa-pause'); icon.classList.add('fa-play'); }
|
||||
if (labelSpan) { labelSpan.textContent = 'Play'; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wire up buttons
|
||||
toggles.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.getAttribute('data-audio-id');
|
||||
const audio = document.getElementById(id);
|
||||
if (!audio) return;
|
||||
|
||||
if (audio.paused) {
|
||||
setAllToStopped(id);
|
||||
audio.play().then(() => updateUI(audio, true)).catch(() => { /* ignore */ });
|
||||
} else {
|
||||
audio.pause();
|
||||
updateUI(audio, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Keep UI in sync with media events
|
||||
audios.forEach(audio => {
|
||||
audio.addEventListener('play', () => {
|
||||
setAllToStopped(audio.id);
|
||||
updateUI(audio, true);
|
||||
});
|
||||
audio.addEventListener('pause', () => updateUI(audio, false));
|
||||
audio.addEventListener('ended', () => {
|
||||
audio.currentTime = 0;
|
||||
updateUI(audio, false);
|
||||
});
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
const slot = document.querySelector(`[data-duration-for="${audio.id}"]`);
|
||||
if (slot && isFinite(audio.duration)) {
|
||||
slot.textContent = `Length: ${formatTime(audio.duration)}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
488
CatherineLynwood/Views/Reckoning/Trailer.cshtml
Normal file
488
CatherineLynwood/Views/Reckoning/Trailer.cshtml
Normal file
@ -0,0 +1,488 @@
|
||||
@model CatherineLynwood.Models.FlagSupportViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "The Alpha Flame - Coming Soon";
|
||||
}
|
||||
|
||||
<!-- Your existing video container: unchanged -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<!-- H1 for SEO and accessibility -->
|
||||
<header class="mb-2 text-center">
|
||||
<h1 class="h3 mb-1">The Alpha Flame: <span class="fw-light">Discovery</span></h1>
|
||||
<p class="mb-1">A gritty Birmingham crime novel set in 1983</p>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="trailer-wrapper">
|
||||
<video id="trailerVideo" playsinline preload="none"></video>
|
||||
<button id="trailerPlayBtn" class="trailer-play-btn">
|
||||
<i class="fad fa-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (DateTime.Now < new DateTime(2025, 8, 21))
|
||||
{
|
||||
<section>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-10 col-md-4 text-center bg-white text-dark border border-dark border-3 rounded-5 mt-3 p-3">
|
||||
<h3 class="h4">Released in:</h3>
|
||||
<div class="release-countdown mb-2" data-release="2025-08-21T00:00:00+01:00" data-out-text="Out now">
|
||||
<span class="rcd d"><span class="num">0</span><span class="label">d</span></span>
|
||||
<span class="rcd h"><span class="num">00</span><span class="label">h</span></span>
|
||||
<span class="rcd m"><span class="num">00</span><span class="label">m</span></span>
|
||||
<span class="rcd s"><span class="num">00</span><span class="label">s</span></span>
|
||||
</div>
|
||||
<div class="d-grid px-3"><a asp-controller="Discovery" asp-action="Index" class="btn btn-dark btn-pulse">Pre-order Now!</a></div>
|
||||
<noscript>Releases on 21 Aug 2025</noscript>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
<!-- Quick interaction: flags -->
|
||||
<section class="container py-3">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<h2 class="h4 mb-2">Please show your support</h2>
|
||||
<p class="mb-3">Tap your flag to show your support.</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="flag-grid" role="group" aria-label="Choose your country">
|
||||
@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";
|
||||
}
|
||||
<button class="flag-btn @pulse" data-country="@code">
|
||||
<img src="/images/flags/@($"{flagFile}.svg")" alt="@name flag">
|
||||
<span class="flag-name">@Html.Raw(name)</span>
|
||||
<span class="flag-count">(@count)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Toast/message area -->
|
||||
<div id="flagToast" class="flag-toast text-center" role="status" aria-live="polite" style="display:none;">
|
||||
<p id="flagMessage" class="mb-2"></p>
|
||||
|
||||
<!-- Hidden release notification form -->
|
||||
<div id="releaseForm" style="display:none;">
|
||||
<p>Want me to let you know when the book is released?</p>
|
||||
<form id="notifyForm" class="mt-2">
|
||||
<input type="text" id="notifyName" name="name" placeholder="Your name (optional)" class="form-control form-control-sm mb-2">
|
||||
<input type="email" id="notifyEmail" name="email" placeholder="Enter your email" class="form-control form-control-sm mb-2" required>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Notify Me</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container py-3">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 text-dark border border-dark border-3 rounded-5 p-4" style="background-image:url('/images/sheet-music.png'); background-position: center; background-size: cover;">
|
||||
<h3 class="h5 text-center text-dark pb-3">Listen to The Alpha Flame theme tune<br />The Flame We Found</h3>
|
||||
<audio controls="controls">
|
||||
<source src="~/audio/the-flame-we-found-original-song-inspired-by-alpha-flame_teaser.mp3" type="audio/mpeg" />
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Teaser reel -->
|
||||
<section class="container py-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 mb-3 text-center">A glimpse inside</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<article class="teaser-card border border-3 border-dark mb-3">
|
||||
<div class="teaser-bg" style="background-image:url('/images/webp/the-alpha-flame-discovery-back-cover-400.webp');"></div>
|
||||
<div class="teaser-copy">
|
||||
<div>
|
||||
<p class="h1 text-warning">
|
||||
The Alpha Flame: Discovery
|
||||
</p>
|
||||
<p>
|
||||
Some girls survive. Others set the world on fire.
|
||||
</p>
|
||||
<p>
|
||||
She didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldn’t walk away.
|
||||
</p>
|
||||
<p>
|
||||
But nothing prepares her for Beth.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="teaser-actions">
|
||||
<button class="btn btn-sm btn-light" data-audio="#aud4"><i class="fad fa-play"></i> Listen 58s</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="aud4" preload="none" src="/audio/book-synopsis.mp3"></audio>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<article class="teaser-card border border-3 border-dark mb-3">
|
||||
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-city-400.webp');"></div>
|
||||
<div class="teaser-copy">
|
||||
<div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
A loud knock on the glass made me jump.
|
||||
</p>
|
||||
<p>
|
||||
“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.
|
||||
</p>
|
||||
<p>
|
||||
My stomach tightened.
|
||||
</p>
|
||||
<p>
|
||||
I wasn’t looking for anything. Not really. But I didn’t drive away either.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="teaser-actions">
|
||||
<button class="btn btn-sm btn-light" data-audio="#aud1"><i class="fad fa-play"></i> Listen 50s</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="aud1" preload="none" src="/audio/snippets/clip-1.mp3"></audio>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<article class="teaser-card border border-3 border-dark mb-3">
|
||||
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-hospital-400.webp');"></div>
|
||||
<div class="teaser-copy">
|
||||
<div>
|
||||
<p>
|
||||
“Maggie… wait.”
|
||||
</p>
|
||||
<p>
|
||||
She turned as I crouched down. My stomach dropped.
|
||||
</p>
|
||||
<p>
|
||||
It was a sweatshirt. Pink. Faded. Cartoon print on the front, cracked with age and wear. Garfield, grinning.
|
||||
</p>
|
||||
<p>
|
||||
I reached out slowly, fingertips brushing the fabric. The left sleeve was soaked, stiff with something dark.
|
||||
</p>
|
||||
<p>
|
||||
Blood.
|
||||
</p>
|
||||
<p>
|
||||
“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.”
|
||||
</p>
|
||||
<p>
|
||||
Maggie dropped to her knees beside me, torch trembling in her grip. “Bloody hell. You’re right.”
|
||||
</p>
|
||||
<p>
|
||||
For a second neither of us moved. The building suddenly felt tighter, like it was watching us.
|
||||
</p>
|
||||
<div class="teaser-actions">
|
||||
<button class="btn btn-sm btn-light" data-audio="#aud2"><i class="fad fa-play"></i> Listen 28s</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="aud2" preload="none" src="/audio/snippets/clip-2.mp3"></audio>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-md-none">
|
||||
<article class="teaser-card border border-3 border-dark mb-2">
|
||||
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-beach-400.webp');"></div>
|
||||
<div class="teaser-copy">
|
||||
<div>
|
||||
<p>
|
||||
She turned in the water, soaked to the waist, flinging droplets everywhere.
|
||||
</p>
|
||||
<p>
|
||||
“Maggie! Come on!” she shouted, laughing. “You’ve got to feel this!”
|
||||
</p>
|
||||
<p>
|
||||
I didn’t hesitate.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
Beth was already dancing through the waves like a lunatic.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="teaser-actions">
|
||||
<button class="btn btn-sm btn-light" data-audio="#aud3"><i class="fad fa-play"></i> Listen 37s</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="aud3" preload="none" src="/audio/snippets/clip-3.mp3"></audio>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer note -->
|
||||
<section class="container pb-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 text-center">
|
||||
<h3 class="h4 mb-3"><strong>Coming 21st August 2025</strong> to major retailers.</h3>
|
||||
<div class="d-grid"><a asp-controller="Discovery" asp-action="Index" class="btn btn-dark btn-pulse">Find Out More</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
@*
|
||||
<responsive-image src="the-alpha-flame-discovery-trailer-landscape.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="100"></responsive-image>
|
||||
<responsive-image src="the-alpha-flame-discovery-trailer-portrait.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
|
||||
<responsive-image src="the-alpha-flame-discovery-back-cover.png" class="img-fluid" alt="The Alpha Flame book cover — gritty 1980s Birmingham crime novel about twin sisters uncovering secrets and surviving abuse" display-width-percentage="50"></responsive-image>
|
||||
*@
|
||||
@section Scripts {
|
||||
<script>
|
||||
const player = new Plyr('audio');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const video = document.getElementById("trailerVideo");
|
||||
const playBtn = document.getElementById("trailerPlayBtn");
|
||||
|
||||
// Pick correct source and poster before loading
|
||||
const isDesktop = window.matchMedia("(min-width: 400px)").matches;
|
||||
video.poster = isDesktop
|
||||
? "/images/webp/the-alpha-flame-discovery-trailer-landscape-1400.webp"
|
||||
: "/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp";
|
||||
const src = isDesktop
|
||||
? "/videos/the-alpha-flame-discovery-trailer-landscape.mp4"
|
||||
: "/videos/the-alpha-flame-discovery-trailer-portrait.mp4";
|
||||
const sourceEl = document.createElement("source");
|
||||
sourceEl.src = src;
|
||||
sourceEl.type = "video/mp4";
|
||||
video.appendChild(sourceEl);
|
||||
|
||||
// Play button click handler
|
||||
playBtn.addEventListener("click", () => {
|
||||
video.muted = false;
|
||||
video.volume = 1.0;
|
||||
video.play().then(() => {
|
||||
playBtn.style.display = "none"; // hide button once playing
|
||||
}).catch(err => {
|
||||
console.warn("Video play failed:", err);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Teaser audio logic ---
|
||||
const btnForAudio = new Map();
|
||||
function anyTeaserPlaying() {
|
||||
return Array.from(btnForAudio.keys()).some(a => !a.paused && !a.ended);
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-audio]").forEach(btn => {
|
||||
const aud = document.querySelector(btn.getAttribute("data-audio"));
|
||||
if (!aud) return;
|
||||
btn.dataset.orig = btn.innerHTML;
|
||||
btnForAudio.set(aud, btn);
|
||||
|
||||
aud.addEventListener("ended", () => {
|
||||
btn.innerHTML = btn.dataset.orig;
|
||||
if (!anyTeaserPlaying()) {
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", e => {
|
||||
const btn = e.target.closest("[data-audio]");
|
||||
if (!btn) return;
|
||||
const aud = document.querySelector(btn.getAttribute("data-audio"));
|
||||
if (!aud) return;
|
||||
|
||||
// Stop others
|
||||
btnForAudio.forEach((b, a) => {
|
||||
if (a !== aud) {
|
||||
a.pause();
|
||||
a.currentTime = 0;
|
||||
b.innerHTML = b.dataset.orig;
|
||||
}
|
||||
});
|
||||
|
||||
if (aud.paused) {
|
||||
video.pause();
|
||||
aud.currentTime = 0;
|
||||
aud.play().then(() => {
|
||||
btn.innerHTML = '<i class="fad fa-pause"></i> Pause';
|
||||
});
|
||||
} else {
|
||||
aud.pause();
|
||||
btn.innerHTML = btn.dataset.orig;
|
||||
if (!anyTeaserPlaying()) {
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let selectedCountry = null;
|
||||
|
||||
window.addEventListener("click", function (e) {
|
||||
const flag = e.target.closest('.flag-btn');
|
||||
if (!flag) return;
|
||||
|
||||
selectedCountry = flag.getAttribute("data-country") || "Your country";
|
||||
const key = "taf_support_" + selectedCountry;
|
||||
|
||||
if (!localStorage.getItem(key)) {
|
||||
localStorage.setItem(key, "1");
|
||||
}
|
||||
|
||||
// Send click to server and update count
|
||||
fetch("/api/support/flag", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ country: selectedCountry })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
// Update the flag's count
|
||||
const countEl = flag.querySelector(".flag-count");
|
||||
if (countEl) countEl.textContent = `(${data.total})`;
|
||||
}
|
||||
});
|
||||
|
||||
// Show thank-you + form
|
||||
document.getElementById("flagMessage").textContent = `Thanks for the love, ${selectedCountry}!`;
|
||||
document.getElementById("flagToast").style.display = "block";
|
||||
document.getElementById("releaseForm").style.display = "block";
|
||||
|
||||
// Tap animation
|
||||
if (flag.animate) {
|
||||
flag.animate(
|
||||
[{ transform: 'scale(1)' }, { transform: 'scale(1.06)' }, { transform: 'scale(1)' }],
|
||||
{ duration: 260, easing: 'ease-out' }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("submit", function (e) {
|
||||
if (e.target && e.target.id === "notifyForm") {
|
||||
e.preventDefault();
|
||||
const emailInput = document.getElementById("notifyEmail");
|
||||
if (!emailInput.value) return;
|
||||
|
||||
fetch("/api/support/subscribe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
country: selectedCountry,
|
||||
name: document.getElementById("notifyName").value || null,
|
||||
email: document.getElementById("notifyEmail").value
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
document.getElementById("releaseForm").innerHTML = "<p>Thanks! We'll email you when the book is released.</p>";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@if (DateTime.Now < new DateTime(2025, 8, 21))
|
||||
{
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
function initCountdown(el) {
|
||||
if (!el) return;
|
||||
var iso = el.getAttribute('data-release');
|
||||
var ts = Date.parse(iso);
|
||||
if (isNaN(ts)) return;
|
||||
|
||||
var dEl = el.querySelector('.rcd.d .num');
|
||||
var hEl = el.querySelector('.rcd.h .num');
|
||||
var mEl = el.querySelector('.rcd.m .num');
|
||||
var sEl = el.querySelector('.rcd.s .num');
|
||||
var outText = el.getAttribute('data-out-text') || 'Out now';
|
||||
|
||||
function pad(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
|
||||
function tick() {
|
||||
var now = Date.now();
|
||||
var diff = ts - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
el.textContent = outText;
|
||||
clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
|
||||
var secs = Math.floor(diff / 1000);
|
||||
var days = Math.floor(secs / 86400); secs -= days * 86400;
|
||||
var hrs = Math.floor(secs / 3600); secs -= hrs * 3600;
|
||||
var mins = Math.floor(secs / 60); secs -= mins * 60;
|
||||
|
||||
if (dEl) dEl.textContent = days;
|
||||
if (hEl) hEl.textContent = pad(hrs);
|
||||
if (mEl) mEl.textContent = pad(mins);
|
||||
if (sEl) sEl.textContent = pad(secs);
|
||||
}
|
||||
|
||||
tick();
|
||||
var timer = setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
// Support multiple countdowns on a page
|
||||
var timers = document.querySelectorAll('.release-countdown');
|
||||
for (var i = 0; i < timers.length; i++) {
|
||||
initCountdown(timers[i]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
29
CatherineLynwood/Views/Reckoning/_Layout.cshtml
Normal file
29
CatherineLynwood/Views/Reckoning/_Layout.cshtml
Normal file
@ -0,0 +1,29 @@
|
||||
@{
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
@section Meta {
|
||||
@RenderSection("Meta", required: false)
|
||||
}
|
||||
|
||||
@section BackgroundVideo {
|
||||
<div id="background-wrapper">
|
||||
<div class="video-background">
|
||||
<video id="siteBackgroundVideo"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
preload="none">
|
||||
<!-- Source will be injected by JS -->
|
||||
</video>
|
||||
<div class="video-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@RenderBody()
|
||||
|
||||
@section Scripts {
|
||||
@RenderSection("Scripts", required: false)
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
@model CatherineLynwood.Models.BuyPanelViewModel
|
||||
|
||||
<div id="buyBox" class="border border-2 border-dark rounded-4 p-3 bg-light mt-auto">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between mb-2">
|
||||
<h3 class="h5 mb-2 mb-sm-0">Buy the Book</h3>
|
||||
<small id="buyCountryHint" class="text-muted d-flex align-items-center">
|
||||
<img id="buyCountryFlag"
|
||||
class="me-1"
|
||||
alt=""
|
||||
width="20"
|
||||
height="14"
|
||||
loading="lazy"
|
||||
src="@Model.FlagUrl" />
|
||||
<span id="buyCountryText">Best options for @Model.ISO2</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@foreach (var group in Model.Groups.OrderBy(x => x.DisplayOrder) )
|
||||
{
|
||||
<div id="rowDirect" class="mb-3">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<div class="row">
|
||||
@if (string.IsNullOrWhiteSpace(group.Message))
|
||||
{
|
||||
<div class="col-12">@group.GroupName</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning">
|
||||
<strong class="alert-heading">@group.GroupName: </strong>
|
||||
@Html.Raw(group.Message)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 align-items-stretch">
|
||||
@foreach (var link in group.Links.OrderBy(x => x.Text))
|
||||
{
|
||||
<div class="col-12 col-sm d-flex">
|
||||
<a class="btn btn-dark w-100 h-100 d-flex justify-content-center align-items-center text-center"
|
||||
href="@link.Target"
|
||||
ping="@($"/track/click?slug={link.Slug}&src={Model.Src}")"
|
||||
rel="nofollow noindex">
|
||||
<span>@Html.Raw(link.Icon) @link.Text</span>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(link.Price))
|
||||
{
|
||||
<span class="price-chip ms-2">@link.Price</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -11,6 +11,7 @@
|
||||
<link rel="stylesheet" href="~/css/duotone.min.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/plyr.min.css" asp-append-version="true" />
|
||||
|
||||
@RenderSection("CSS", required: false)
|
||||
|
||||
<style>
|
||||
.plyr--audio .plyr__controls {
|
||||
@ -57,20 +58,19 @@
|
||||
|
||||
</head>
|
||||
<body class="bg-primary text-white">
|
||||
<div id="background-wrapper">
|
||||
<div class="video-background">
|
||||
<video id="siteBackgroundVideo"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
preload="none"
|
||||
poster="/images/webp/the-alpha-flame-discovery-blank-400.webp">
|
||||
<!-- Source will be injected by JS -->
|
||||
</video>
|
||||
<div class="video-overlay"></div>
|
||||
@if (IsSectionDefined("BackgroundVideo"))
|
||||
{
|
||||
@RenderSection("BackgroundVideo", required: false)
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="background-wrapper">
|
||||
<div class="video-background">
|
||||
<div class="static-background"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="content">
|
||||
@ -98,12 +98,6 @@
|
||||
The Alpha Flame
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="Discovery" asp-action="Index">Discovery (Book 1)</a>
|
||||
</li>
|
||||
@* <li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="Discovery" asp-action="Trailer">Discovery Release Trailer</a>
|
||||
</li> *@
|
||||
<li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Index">The Trilogy</a>
|
||||
</li>
|
||||
@ -113,11 +107,23 @@
|
||||
<li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Characters">Meet the Characters</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
<li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="Discovery" asp-action="Index">The Alpha Flame: Discovery</a>
|
||||
</li>
|
||||
<li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="Reckoning" asp-action="Index">The Alpha Flame: Reckoning</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="py-2">
|
||||
<a class="dropdown-item" asp-controller="Collaborations" asp-action="LarhysaSaddul">Larhysa Saddul - Narrator</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@* <li class="nav-item">
|
||||
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="ArcReaderApplication">ARC Reader Application</a>
|
||||
</li>
|
||||
</li> *@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="VerosticGenre">The Verostic Genre</a>
|
||||
</li>
|
||||
@ -215,28 +221,77 @@
|
||||
<script src="~/js/plyr.js"></script>
|
||||
|
||||
|
||||
@* <script>
|
||||
const player = new Plyr('audio');
|
||||
</script> *@
|
||||
@RenderSection("Scripts", required: false)
|
||||
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
setTimeout(() => {
|
||||
(function () {
|
||||
const mediaMap = {
|
||||
discovery: {
|
||||
video: "/videos/background-discovery.mp4",
|
||||
poster: "/images/webp/the-alpha-flame-discovery-blank-400.webp"
|
||||
},
|
||||
reckoning: {
|
||||
video: "/videos/background-reckoning.mp4",
|
||||
poster: "/images/webp/the-alpha-flame-reckoning-blank-400.webp"
|
||||
},
|
||||
redemption: {
|
||||
video: "/videos/background-redemption.mp4",
|
||||
poster: "/images/webp/the-alpha-flame-redemption-blank-400.webp"
|
||||
},
|
||||
default: {
|
||||
video: "/videos/background-reckoning.mp4",
|
||||
poster: "/images/webp/the-alpha-flame-reckoning-blank-400.webp"
|
||||
}
|
||||
};
|
||||
|
||||
function pickMedia(pathname) {
|
||||
const path = (pathname || "/").toLowerCase();
|
||||
if (path.includes("/discovery")) return mediaMap.discovery;
|
||||
if (path.includes("/reckoning")) return mediaMap.reckoning;
|
||||
if (path.includes("/redemption")) return mediaMap.redemption;
|
||||
return mediaMap.default;
|
||||
}
|
||||
|
||||
function initBackgroundVideo() {
|
||||
const video = document.getElementById("siteBackgroundVideo");
|
||||
if (!video) return;
|
||||
|
||||
const chosen = pickMedia(window.location.pathname);
|
||||
|
||||
// Stop any eager autoplay behaviour while we swap assets
|
||||
try { video.pause(); } catch {}
|
||||
|
||||
// Ensure poster matches route BEFORE loading anything
|
||||
video.poster = chosen.poster;
|
||||
|
||||
// Clear any existing sources to avoid weird caching/duplication issues
|
||||
video.querySelectorAll("source").forEach(s => s.remove());
|
||||
|
||||
const source = document.createElement("source");
|
||||
source.src = "/videos/background-3.mp4";
|
||||
source.src = chosen.video;
|
||||
source.type = "video/mp4";
|
||||
video.appendChild(source);
|
||||
video.load(); // Initiates download
|
||||
|
||||
// Now load and play
|
||||
video.load();
|
||||
video.play().catch(err => {
|
||||
console.warn("Background video autoplay failed:", err);
|
||||
});
|
||||
}, 1500); // Adjust delay as needed (e.g. 1000–3000ms)
|
||||
});
|
||||
}
|
||||
|
||||
// Run ASAP once the DOM exists
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initBackgroundVideo);
|
||||
} else {
|
||||
initBackgroundVideo();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
|
||||
@ -28,13 +28,13 @@
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-lg border-3 border-dark">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-3 d-none d-md-block">
|
||||
<a href="/the-alpha-flame/discovery">
|
||||
<div class="col-md-3 d-none d-md-block border-end border-dark border-3">
|
||||
<a asp-controller="Discovery" asp-action="Index">
|
||||
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-start-3" alt="The Alpha Flame: Discovery" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 d-md-none">
|
||||
<a href="/the-alpha-flame/discovery">
|
||||
<a asp-controller="Discovery" asp-action="Index">
|
||||
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-top-3" alt="The Alpha Flame: Discovery" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
</div>
|
||||
@ -43,7 +43,7 @@
|
||||
<h3 class="card-title">The Alpha Flame: <span class="fw-light">Discovery</span></h3>
|
||||
<p class="fst-italic">Some girls survive. Others set the world on fire.</p>
|
||||
<p>
|
||||
Maggie didn’t go looking for trouble. But when she found Beth—bruised, broken, and terrified—she couldn’t walk away.
|
||||
Maggie didn’t go looking for trouble. But when she found Beth, bruised, broken, and terrified, she couldn’t walk away.
|
||||
</p>
|
||||
<p>
|
||||
What begins as a rescue becomes something far more dangerous. Drawn into Beth’s world, Maggie is forced to confront a hidden undercurrent of control, abuse, and corruption that runs deeper than she ever imagined. A seedy underworld of silence and survival where nothing is as it seems, and walking away is no longer an option.
|
||||
@ -52,10 +52,10 @@
|
||||
As truths unravel and pasts collide, Maggie must decide who she is, who she trusts, and how far she's willing to go for someone she barely knows… yet feels inexplicably bound to.
|
||||
</p>
|
||||
<p class="fw-semibold">
|
||||
<em>The Alpha Flame: Discovery</em> is a gritty, emotionally charged thriller about trauma, loyalty, and fire—forged in pain, and burning with truth.
|
||||
<em>The Alpha Flame: Discovery</em> is a gritty, emotionally charged thriller about trauma, loyalty, and fire, forged in pain, and burning with truth.
|
||||
</p>
|
||||
<div class="mt-auto text-end">
|
||||
<a href="/the-alpha-flame/discovery" class="btn btn-dark">Read More</a>
|
||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -65,6 +65,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-start border-3 border-dark position-relative">
|
||||
<span class="badge bg-warning text-dark position-absolute top-0 start-0 m-2">Coming 1st April 2026</span>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-3 d-none d-md-block border-end border-dark border-3">
|
||||
<a asp-controller="Reckoning" asp-action="Index">
|
||||
<responsive-image src="the-alpha-flame-reckoning-cover.png" class="img-fluid rounded-start-3" alt="The Alpha Flame: Reckoning" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 d-md-none">
|
||||
<a asp-controller="Reckoning" asp-action="Index">
|
||||
<responsive-image src="the-alpha-flame-reckoning-cover.png" class="img-fluid rounded-top-3" alt="The Alpha Flame: Reckoning" display-width-percentage="50"></responsive-image>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">The Alpha Flame: <span class="fw-light">Reckoning</span></h3>
|
||||
<p class="fst-italic">Two flames, once divided, now burn as one.</p>
|
||||
|
||||
<p>
|
||||
The past refuses to stay buried. As Maggie and Beth dig deeper into the truth their mother left behind, what they uncover is far more dangerous than either of them expected.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Bound by blood and scarred by what they’ve survived, the sisters follow a trail of secrets through corruption, violence, and long-hidden crimes that reach into the heart of Birmingham’s underworld. Every answer raises the stakes. Every step forward brings new eyes watching from the shadows.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When justice finally comes into view, it demands a price. Maggie must decide how far she’s willing to go, who she’s willing to lose, and whether survival is enough when the truth threatens to burn everything down.
|
||||
</p>
|
||||
|
||||
<p class="fw-semibold">
|
||||
<em>The Alpha Flame: Reckoning</em> is a dark, emotionally charged continuation of the trilogy, where loyalty is tested, love is weaponised, and the fire that once protected now demands its due.
|
||||
</p>
|
||||
|
||||
<div class="mt-auto text-end">
|
||||
<a asp-controller="Reckoning" asp-action="Index" class="btn btn-dark">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@*
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-start border-3 border-dark position-relative">
|
||||
@ -91,58 +140,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
*@
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-start border-3 border-dark position-relative">
|
||||
<span class="badge bg-warning text-dark position-absolute top-0 start-0 m-2">Coming Spring 2026</span>
|
||||
<span class="badge bg-warning text-dark position-absolute top-0 start-0 m-2">Coming November 2026</span>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
<responsive-image src="the-alpha-flame-10.png" class="img-fluid rounded-start-3 placeholder-blur" alt="The Alpha Flame: Reckoning" display-width-percentage="50"></responsive-image>
|
||||
<div class="col-md-3 d-none d-md-block border-end border-dark border-3">
|
||||
<responsive-image src="the-alpha-flame-redemption-cover.png" class="img-fluid rounded-start-3" alt="The Alpha Flame: Redemption" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="col-md-2 d-md-none">
|
||||
<responsive-image src="the-alpha-flame-10.png" class="img-fluid rounded-top-3 placeholder-blur" alt="The Alpha Flame: Reckoning" display-width-percentage="50"></responsive-image>
|
||||
<div class="col-md-3 d-md-none">
|
||||
<responsive-image src="the-alpha-flame-redemption-cover.png" class="img-fluid rounded-top-3" alt="The Alpha Flame: Redemption" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">The Alpha Flame: <span class="fw-light">Reckoning</span></h5>
|
||||
<p class="card-text">
|
||||
As truths begin to unravel, *The Alpha Flame: Reckoning* plunges deeper into the aftermath of choices made and sins buried.
|
||||
The cost of survival is rising, and the ghosts of the past demand payment in full.
|
||||
</p>
|
||||
<p class="card-text">
|
||||
With loyalties tested and new threats circling, the flame that once brought light now threatens to consume. Darker, more daring, and laced with tension, the second book refuses to flinch from the consequences of truth.
|
||||
</p>
|
||||
<div class="mt-auto text-end">
|
||||
<a href="#" class="btn btn-dark disabled">Coming 2026</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-start border-3 border-dark position-relative">
|
||||
<span class="badge bg-warning text-dark position-absolute top-0 start-0 m-2">Coming Autumn 2026</span>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
<responsive-image src="the-alpha-flame-10.png" class="img-fluid rounded-start-3 placeholder-blur" alt="The Alpha Flame: Redemption" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="col-md-2 d-md-none">
|
||||
<responsive-image src="the-alpha-flame-10.png" class="img-fluid rounded-top-3 placeholder-blur" alt="The Alpha Flame: Redemption" display-width-percentage="50"></responsive-image>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="col-md-9">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">The Alpha Flame: <span class="fw-light">Redemption</span></h5>
|
||||
<p class="card-text">
|
||||
Every fire must either burn out or forge something stronger. *The Alpha Flame: Redemption* is a story of confronting what’s been lost, what’s been taken, and what might still be saved.
|
||||
</p>
|
||||
<p class="card-text">
|
||||
As the final pieces fall into place, the women at the heart of the flame must decide if healing is possible—or if some wounds are meant to stay open. Fierce, emotional, and defiantly human, this final chapter brings the trilogy to a powerful close.
|
||||
As the final pieces fall into place, the women at the heart of the flame must decide if healing is possible, or if some wounds are meant to stay open. Fierce, emotional, and defiantly human, this final chapter brings the trilogy to a powerful close.
|
||||
</p>
|
||||
<div class="mt-auto text-end">
|
||||
<a href="#" class="btn btn-dark disabled">Coming 2026</a>
|
||||
|
||||
BIN
CatherineLynwood/wwwroot/audio/Excerpts-1.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/Excerpts-1.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/audio/Excerpts-2.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/Excerpts-2.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/audio/Excerpts-3.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/Excerpts-3.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/audio/discovery-clip-1.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/discovery-clip-1.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/audio/discovery-clip-2.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/discovery-clip-2.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/audio/discovery-clip-3.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/discovery-clip-3.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/audio/larhysa-Interview.mp3
Normal file
BIN
CatherineLynwood/wwwroot/audio/larhysa-Interview.mp3
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/alpha-flame-audio-coming.png
Normal file
BIN
CatherineLynwood/wwwroot/images/alpha-flame-audio-coming.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
CatherineLynwood/wwwroot/images/beware-book-promotion-scams.png
Normal file
BIN
CatherineLynwood/wwwroot/images/beware-book-promotion-scams.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
CatherineLynwood/wwwroot/images/book-club-guide-alpha-flame.png
Normal file
BIN
CatherineLynwood/wwwroot/images/book-club-guide-alpha-flame.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
CatherineLynwood/wwwroot/images/bridge-over-the-river.png
Normal file
BIN
CatherineLynwood/wwwroot/images/bridge-over-the-river.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
CatherineLynwood/wwwroot/images/flags/ad.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ad.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ad.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ad.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ae.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ae.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ae.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ae.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/af.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/af.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/af.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/af.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ag.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ag.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ag.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ag.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ai.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ai.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ai.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ai.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/al.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/al.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/al.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/al.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/am.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/am.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/am.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/am.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ao.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ao.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ao.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ao.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/aq.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/aq.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/aq.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/aq.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ar.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ar.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ar.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ar.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/arab.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/arab.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/arab.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/arab.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/as.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/as.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/as.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/as.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/asean.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/asean.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/asean.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/asean.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/at.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/at.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/at.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/at.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/au.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/au.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/au.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/au.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/aw.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/aw.svg.br
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/aw.svg.gz
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/aw.svg.gz
Normal file
Binary file not shown.
BIN
CatherineLynwood/wwwroot/images/flags/ax.svg.br
Normal file
BIN
CatherineLynwood/wwwroot/images/flags/ax.svg.br
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user