This commit is contained in:
Nick 2025-09-06 23:01:22 +01:00
parent f0e5052e2c
commit 31ae1e80ea
50 changed files with 1536 additions and 50 deletions

View File

@ -0,0 +1,240 @@
using CatherineLynwood.Models;
using CatherineLynwood.Services;
using Microsoft.AspNetCore.Mvc;
using System.Web;
namespace CatherineLynwood.Controllers
{
[Route("go")]
public class BuyController : Controller
{
private readonly DataAccess _dataAccess;
// Replace with DB/service
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 (all countries) ---
["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"
}
};
public BuyController(DataAccess dataAccess)
{
_dataAccess = dataAccess;
}
[HttpGet("{slug}")]
public async Task<IActionResult> Go(string slug)
{
if (!Links.TryGetValue(slug, out var link))
return NotFound();
var referer = Request.Headers["Referer"].ToString();
var ua = Request.Headers["User-Agent"].ToString();
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
var qs = Request.QueryString.HasValue ? Request.QueryString.Value : string.Empty;
var page = referer; // or: $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}"
var countryQS = Request.Query["country"].ToString(); // if you pass ?country=GB for testing
// session id cookie
var sessionId = Request.Cookies["sid"] ?? Guid.NewGuid().ToString("N");
if (!Request.Cookies.ContainsKey("sid"))
{
Response.Cookies.Append("sid", sessionId, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = true,
MaxAge = TimeSpan.FromDays(365)
});
}
// Save click (dont block the redirect if this fails)
_ = await _dataAccess.SaveBuyClick(
dateTimeUtc: DateTime.UtcNow,
slug: link.Slug,
retailer: link.Retailer,
format: link.Format,
countryGroup: link.CountryGroup,
ip: ip,
country: string.IsNullOrWhiteSpace(countryQS) ? null : countryQS, // optional override
userAgent: ua,
referer: referer,
page: page,
sessionId: sessionId,
queryString: qs,
destinationUrl: link.Url
);
return Redirect(link.Url); // 302
}
}
}

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace CatherineLynwood.Controllers
@ -74,7 +75,7 @@ namespace CatherineLynwood.Controllers
Reviews reviews = await _dataAccess.GetReviewsAsync();
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
return View(reviews);
return View("Index1", reviews);
}
[BookAccess(1, 1)]
@ -107,11 +108,50 @@ namespace CatherineLynwood.Controllers
return View();
}
[BookAccess(1, 1)]
[Route("extras/soundtrack")]
public async Task<IActionResult> Soundtrack()
{
List<SoundtrackTrackModel> soundtrackTrackModels = await _dataAccess.GetSoundtrack();
return View(soundtrackTrackModels);
}
[Route("trailer")]
public async Task<IActionResult> Trailer()
{
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
//ip = "77.104.168.236";
string country = "Unknown";
using (var client = new HttpClient())
{
try
{
var response = await client.GetStringAsync($"http://ip-api.com/json/{ip}");
var json = JsonDocument.Parse(response);
country = json.RootElement.GetProperty("countryCode").GetString();
if (country == "GB")
{
country = "UK";
}
}
catch (Exception ex)
{
// Fail silently
}
}
FlagSupportViewModel flagSupportViewModel = await _dataAccess.GetTrailerLikes();
foreach (var flag in flagSupportViewModel.FlagCounts)
{
flag.Selected = flag.Key.ToLower() == country.ToLower();
}
return View(flagSupportViewModel);
}

View File

@ -42,6 +42,7 @@ namespace CatherineLynwood.Controllers
new SitemapEntry { Url = Url.Action("Characters", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("ContactCatherine", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Giveaways", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("StoryMap", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("HowToBuy", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "AskAQuestion", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
new SitemapEntry { Url = Url.Action("Index", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },

View File

@ -256,6 +256,12 @@ namespace CatherineLynwood.Controllers
return View();
}
[Route("story-map")]
public IActionResult StoryMap()
{
return View();
}
#endregion Public Methods
#region Private Methods

View File

@ -0,0 +1,19 @@
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
}
}

View File

@ -2,7 +2,16 @@
{
public class FlagSupportViewModel
{
public Dictionary<string, int> FlagCounts { get; set; } = new();
public List<FlagCount> FlagCounts { get; set; } = new();
}
public class FlagCount
{
public string Key { get; set; }
public int Value { get; set; }
public bool Selected { get; set; }
}
}

View File

@ -0,0 +1,27 @@
namespace CatherineLynwood.Models
{
public class SoundtrackTrackModel
{
#region Public Properties
public string AudioUrl { get; set; }
public string Chapter { get; set; }
// e.g., "Chapter 18"
public string Description { get; set; }
// short blurb
public string ImageUrl { get; set; }
// artwork or suggestive image
// mp3/ogg
public string LyricsHtml { get; set; }
public string Title { get; set; }
#endregion Public Properties
// fallback if no HTML
}
}

View File

@ -65,6 +65,64 @@ namespace CatherineLynwood.Services
return visible;
}
public async Task<bool> SaveBuyClick(
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
)
{
bool success = true;
using (SqlConnection conn = new SqlConnection(_connectionString))
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "SaveBuyClick";
// 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
cmd.Parameters.AddWithValue("@IP", (object?)ip ?? DBNull.Value);
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);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception)
{
success = false;
// optional: log the exception somewhere central
}
}
return success;
}
public async Task<bool> Addhoneypot(DateTime dateTime, string ip, string country, string userAgent, string referer)
{
bool success = true;
@ -131,6 +189,48 @@ namespace CatherineLynwood.Services
return likes;
}
public async Task<List<SoundtrackTrackModel>> GetSoundtrack()
{
List<SoundtrackTrackModel> soundtrackTrackModels = new List<SoundtrackTrackModel>();
using (SqlConnection conn = new SqlConnection(_connectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
try
{
await conn.OpenAsync();
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "GetSoundtrack";
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
{
while (await rdr.ReadAsync())
{
soundtrackTrackModels.Add(new SoundtrackTrackModel
{
AudioUrl = GetDataString(rdr, "AudioUrl"),
Chapter = GetDataString(rdr, "Chapter"),
Description = GetDataString(rdr, "Description"),
ImageUrl = GetDataString(rdr, "ImageUrl"),
LyricsHtml = GetDataString(rdr, "LyricsHtml"),
Title = GetDataString(rdr, "Title")
});
}
}
}
catch (Exception ex)
{
}
}
}
return soundtrackTrackModels;
}
public async Task<FlagSupportViewModel> GetTrailerLikes()
{
FlagSupportViewModel flagSupportViewModel = new FlagSupportViewModel();
@ -153,7 +253,11 @@ namespace CatherineLynwood.Services
string country = GetDataString(rdr, "Country");
int likes = GetDataInt(rdr, "Likes");
flagSupportViewModel.FlagCounts.Add(country, likes);
flagSupportViewModel.FlagCounts.Add(new FlagCount
{
Key = country,
Value = likes
});
}
}
}

View File

@ -43,6 +43,14 @@
<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)
{

View File

@ -44,15 +44,13 @@
<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>
<!-- IngramSpark direct paperback link placeholder -->
<a href="#" class="btn btn-outline-dark mb-2 disabled" title="Coming soon">
📦 Buy Direct (Save & Support Author)
</a>
<p class="small text-muted mb-0">ISBN 978-1-0682258-1-9</p>
<p class="small text-muted" id="extraRetailers"></p>
</div>
@ -67,15 +65,14 @@
<p>
A premium collectors 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>
<!-- IngramSpark direct hardback link placeholder -->
<a href="#" class="btn btn-outline-dark mb-2 disabled" title="Coming soon">
💎 Buy Direct (Save & Support Author)
</a>
<p class="small text-muted mb-0">ISBN 978-1-0682258-0-2</p>
<p class="small text-muted" id="extraRetailersHardback"></p>
</div>
@ -86,14 +83,12 @@
<div class="card-header">
<i class="fad fa-headphones-alt text-info me-2"></i> Audiobook (AI-Read)
</div>
<div class="card-body text-center">
<p class="mb-2">Listen to the entire book for free on ElevenLabs:</p>
<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 />
<!-- Optional QR -->
<img src="/images/discovery-qrcode.png" alt="Scan to listen" class="img-fluid" style="max-width: 120px;" />
</div>
</div>
</div>
@ -140,11 +135,29 @@
break;
}
document.getElementById("kindleLink").setAttribute("href", kindleLink);
document.getElementById("paperbackLink").setAttribute("href", paperbackLink);
document.getElementById("hardbackLink").setAttribute("href", hardbackLink);
document.getElementById("extraRetailers").innerHTML = extraRetailers;
document.getElementById("extraRetailersHardback").innerHTML = extraRetailersHardback;
// 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>

View File

@ -45,18 +45,9 @@
</div>
<div class="card-body" id="companion-body">
<!-- Buy Section -->
<div class="p-2">
<h2 class="h5">Buy the Book</h2>
</div>
<div id="buy-now" class="mb-4">
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" target="_blank" class="btn btn-dark mb-2">
Buy Kindle Edition
</a>
<p class="text-muted small mb-1">
Want paperback, hardback, or audiobook options?
</p>
<a asp-action="HowToBuy" class="btn btn-outline-dark btn-sm">
See All Buying Options
<a asp-action="HowToBuy" class="btn btn-dark">
<i class="fad fa-books"> </i> Buy the Book
</a>
</div>

View File

@ -0,0 +1,469 @@
@model CatherineLynwood.Models.Reviews
@{
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
bool showReviews = Model.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>
<!-- HERO: Cover + Trailer + Buy Box -->
<section class="mb-4">
<div class="row g-3 align-items-stretch">
<!-- Book Cover -->
<div class="col-md-5 d-flex">
<div class="card character-card h-100 flex-fill" id="cover-card">
<responsive-image src="the-alpha-flame-discovery-cover.png"
class="card-img-top"
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>
<div class="card-body border-top border-3 border-dark">
<h3 class="card-title h5 mb-1">The Alpha Flame: <span class="fw-light">Discovery</span></h3>
<p class="card-text mb-0">Maggie stands outside the derelict Rubery Hill Hospital. 1983 Birmingham. A story of survival, sisters, and fire.</p>
</div>
</div>
</div>
<!-- Trailer + Buy Box -->
<div class="col-md-7 d-flex">
<div class="card character-card h-100 flex-fill" id="hero-media-card">
<div class="card-body d-flex flex-column">
<!-- Trailer -->
<div class="trailer-wrapper mb-3">
<video id="trailerVideo" playsinline preload="none" class="w-100"></video>
<button id="trailerPlayBtn" class="trailer-play-btn">
<i class="fad fa-play"></i>
</button>
</div>
<!-- Compact trust panel -->
<div class="mb-3">
<div class="border border-2 border-dark rounded-4 p-3 bg-light">
<div class="small text-muted text-uppercase mb-2">Formats & delivery</div>
<ul class="list-unstyled small mb-0">
<li class="mb-1"><i class="fad fa-gem me-1"></i> Hardback, collectors edition</li>
<li class="mb-1"><i class="fad fa-book me-1"></i> Paperback, bookshop edition</li>
<li class="mb-1"><i class="fad fa-shipping-fast me-1"></i> Fast fulfilment in GB and US via our print partner</li>
<li><i class="fad fa-store me-1"></i> Also available from Amazon, Waterstones, Barnes & Noble</li>
</ul>
</div>
</div>
<!-- Buy Box -->
<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 class="text-muted">Best options for your country</small>
</div>
<!-- Row 1: Direct via Ingram (GB/US only) -->
<div id="rowDirect" class="mb-3">
<div class="d-flex align-items-center gap-2 mb-2">
<span class="badge bg-dark">Best price</span>
<span class="small text-muted">Direct via print partner</span>
</div>
<div class="row g-2">
<div class="col-12 col-sm-6">
<!-- Default GB slug; script swaps to -us where applicable -->
<a id="hardbackLinkSelf"
href="/go/ingram-hardback-gb"
target="_blank"
class="btn btn-dark w-100"
data-retailer="Ingram" data-format="Hardback">
<i class="fad fa-gem me-1"></i> Hardback, direct
</a>
</div>
<div class="col-12 col-sm-6">
<a id="paperbackLinkSelf"
href="/go/ingram-paperback-gb"
target="_blank"
class="btn btn-dark w-100"
data-retailer="Ingram" data-format="Paperback">
<i class="fad fa-book me-1"></i> Paperback, direct
</a>
</div>
</div>
</div>
<!-- Row 2: Amazon -->
<div id="rowAmazon" class="mb-3">
<div class="row g-2">
<div class="col-12 col-sm-6">
<a id="hardbackLink"
href="/go/amazon-hardback-gb"
target="_blank"
class="btn btn-outline-dark w-100"
data-retailer="Amazon" data-format="Hardback">
<i class="fad fa-gem me-1"></i> Hardback, Amazon
</a>
</div>
<div class="col-12 col-sm-6">
<a id="paperbackLink"
href="/go/amazon-paperback-gb"
target="_blank"
class="btn btn-outline-dark w-100"
data-retailer="Amazon" data-format="Paperback">
<i class="fad fa-book me-1"></i> Paperback, Amazon
</a>
</div>
</div>
</div>
<!-- Row 3: National retailer (Waterstones in GB, B&N in US) -->
<div id="rowNational" class="mb-2">
<div class="row g-2">
<div class="col-12 col-sm-6">
<a id="hardbackNational"
href="/go/waterstones-hardback-gb"
target="_blank"
class="btn btn-outline-dark w-100"
data-retailer="National" data-format="Hardback">
<i class="fad fa-gem me-1"></i> Hardback, Waterstones
</a>
</div>
<div class="col-12 col-sm-6">
<a id="paperbackNational"
href="/go/waterstones-paperback-gb"
target="_blank"
class="btn btn-outline-dark w-100"
data-retailer="National" data-format="Paperback">
<i class="fad fa-book-open me-1"></i> Paperback, Waterstones
</a>
</div>
</div>
</div>
<!-- Quiet eBook -->
<div class="mt-1 small">
Prefer Kindle? <a id="kindleLink" href="/go/amazon-kindle-gb" target="_blank" class="link-dark" data-retailer="Amazon" data-format="Kindle">Buy the eBook</a>
</div>
<div class="mt-2">
<a asp-action="HowToBuy" class="link-dark small">See all buying options</a>
</div>
</div>
</div>
</div>
<!-- Sticky mobile buy bar -->
<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>
</div>
</div>
</section>
<!-- Social proof: show one standout review near the top -->
@if (showReviews)
{
var top = Model.Items.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.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>
}
<!-- Synopsis: short teaser with collapse for the full version -->
<section id="synopsis" class="mb-4">
<div class="card character-card">
<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-synopsis.mp3" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
<p class="text-center text-muted small mb-0">Listen to Catherine talking about the book</p>
</div>
</div>
<!-- Teaser paragraph -->
<p class="card-text">
Set in 1983 Birmingham, The Alpha Flame: Discovery follows twin sisters 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>
<!-- Collapse trigger -->
<p class="mb-2">
<a class="btn btn-outline-dark btn-sm" data-bs-toggle="collapse" href="#fullSynopsis" role="button" aria-expanded="false" aria-controls="fullSynopsis">
Read full synopsis
</a>
</p>
<!-- Full synopsis in collapse -->
<div class="collapse" id="fullSynopsis">
<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. Beths 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 doesnt just live; she races through life, fuelled by a need for speed and the thrill of freedom. Her Triumph TR6 isnt just a car; its 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 Maggies intensity doesnt 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 isnt 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 shes 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>
<!-- 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...</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>
<!-- Trailer source selection + play button -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const video = document.getElementById("trailerVideo");
const playBtn = document.getElementById("trailerPlayBtn");
if (!video || !playBtn) return;
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);
playBtn.addEventListener("click", () => {
video.muted = false;
video.volume = 1.0;
video.play().then(() => {
playBtn.style.display = "none";
}).catch(err => {
console.warn("Video play failed:", err);
});
});
});
</script>
<!-- Geo-based slug swap for /go/{retailer}-{format}-{country} + dev override -->
<script>
(function() {
function qsCountryOverride() {
const qs = new URLSearchParams(location.search);
const c = qs.get('country');
return c ? c.toUpperCase() : null;
}
// Replace trailing "-gb" in slugs with target country code
function swapCountrySlug(a, toCountry) {
if (!a || !a.href) return;
try {
const url = new URL(a.href, location.origin);
// Slugs are like: /go/amazon-hardback-gb
url.pathname = url.pathname.replace(/-(gb|us|ca|au)$/i, '-' + toCountry.toLowerCase());
// Also allow passing country through to /go for logging/testing
url.searchParams.set('country', toCountry);
a.href = url.pathname + url.search;
} catch(e) { /* noop */ }
}
fetch('https://ipapi.co/json/')
.then(r => r.json())
.then(data => {
const override = qsCountryOverride();
const country = (override || data.country_code || 'US').toUpperCase();
// Core anchors
const elKindle = document.getElementById("kindleLink");
const hbAmz = document.getElementById("hardbackLink");
const pbAmz = document.getElementById("paperbackLink");
const hbNat = document.getElementById("hardbackNational");
const pbNat = document.getElementById("paperbackNational");
const rowNat = document.getElementById("rowNational");
const rowDirect = document.getElementById("rowDirect");
const hbSelf = document.getElementById("hardbackLinkSelf");
const pbSelf = document.getElementById("paperbackLinkSelf");
// Always swap Amazon & Kindle to target country
[hbAmz, pbAmz, elKindle].forEach(a => swapCountrySlug(a, country));
// Ingram Direct row only for GB/US
const showDirect = country === "GB" || country === "US";
if (rowDirect) rowDirect.classList.toggle("d-none", !showDirect);
if (showDirect) {
[hbSelf, pbSelf].forEach(a => swapCountrySlug(a, country)); // swap to -gb or -us
} else {
// If direct hidden, make Amazon buttons solid
[hbAmz, pbAmz].forEach(el => el && el.classList.replace("btn-outline-dark", "btn-dark"));
}
// National row: Waterstones (GB) or Barnes & Noble (US). Hidden elsewhere.
if (country === "GB") {
// Keep Waterstones slugs (already -gb), but ensure '?country=GB' param is present
[hbNat, pbNat].forEach(a => swapCountrySlug(a, country));
if (hbNat) hbNat.textContent = "Hardback, Waterstones";
if (pbNat) pbNat.textContent = "Paperback, Waterstones";
if (rowNat) rowNat.classList.remove("d-none");
} else if (country === "US") {
// Switch retailer in slug: waterstones-...-gb -> bn-...-us
function switchToBN(a, format) {
if (!a) return;
try {
const url = new URL(a.href, location.origin);
url.pathname = url.pathname
.replace(/waterstones-(hardback|paperback)-(gb|us|ca|au)/i, (m, _fmt) => {
return `bn-${format.toLowerCase()}-us`;
});
url.searchParams.set('country', 'US');
a.href = url.pathname + url.search;
} catch(e) {}
}
switchToBN(hbNat, "Hardback");
switchToBN(pbNat, "Paperback");
if (hbNat) hbNat.textContent = "Hardback, Barnes & Noble";
if (pbNat) pbNat.textContent = "Paperback, Barnes & Noble";
if (rowNat) rowNat.classList.remove("d-none");
} else {
if (rowNat) rowNat.classList.add("d-none");
}
})
.catch(() => {
// If geo fails, GB defaults remain; users can still click, and /go logs with country=GB
});
})();
</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-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, 06)"
twitter-card-type="summary_large_image"
twitter-site-handle="@@CathLynwood"
twitter-creator-handle="@@CathLynwood" />
<script type="application/ld+json">
@Html.Raw(Model.SchemaJsonLd)
</script>
}

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

View File

@ -11,7 +11,7 @@
<!-- 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="text-muted mb-0">A gritty Birmingham crime novel set in 1983</p>
<p class="mb-1">A gritty Birmingham crime novel set in 1983</p>
</header>
</div>
</div>
@ -27,17 +27,39 @@
</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">Show your support</h2>
<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
@ -55,7 +77,12 @@
"uk" => "gb",
_ => code.ToLower()
};
<button class="flag-btn" data-country="@code">
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>
@ -82,7 +109,16 @@
</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 -->
@ -92,12 +128,61 @@
<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 didnt go looking for trouble. But when she found Beth, bruised, broken, and terrified, Maggie couldnt walk away.
</p>
<p>
But nothing prepares her for Beth.
</p>
<p>
As she digs deeper into Beths 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 isnt someone elses nightmare. Its 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, its 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>“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>
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 wasnt looking for anything. Not really. But I didnt 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>
@ -111,7 +196,30 @@
<div class="teaser-bg" style="background-image:url('/images/webp/teaser-hospital-400.webp');"></div>
<div class="teaser-copy">
<div>
<p>“Maggie…” My voice broke. “Its hers. She used to wear this all the time. She was wearing it the last time I saw her.”</p>
<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. “Its 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. Youre 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>
@ -121,12 +229,30 @@
</article>
</div>
<div class="col-md-4">
<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>Waves erased our footprints; morning would come. So would he.</p>
<p>
She turned in the water, soaked to the waist, flinging droplets everywhere.
</p>
<p>
“Maggie! Come on!” she shouted, laughing. “Youve got to feel this!”
</p>
<p>
I didnt 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 wed somehow got all those childhood summers back in one moment.
The sea was freezing, but we didnt 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>
@ -150,11 +276,15 @@
@* <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-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", () => {
@ -162,7 +292,7 @@
const playBtn = document.getElementById("trailerPlayBtn");
// Pick correct source and poster before loading
const isDesktop = window.matchMedia("(min-width: 992px)").matches;
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";
@ -302,7 +432,57 @@
});
</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>
}
}

View File

@ -13,9 +13,12 @@
<style>
.plyr--audio .plyr__controls {
background-color: transparent !important; /* or set your page colour */
background-color: rgba(255,255,255,1) !important; /* or set your page colour */
box-shadow: none !important; /* optional: remove shadow */
border-radius: 0; /* optional: adjust corners */
border-radius: 15px; /* optional: adjust corners */
border-color: black;
border-width: 2px;
border-style: solid;
}
.plyr--audio {
@ -103,6 +106,9 @@
<li class="py-2">
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Index">The Trilogy</a>
</li>
<li class="py-2">
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="StoryMap">Story Map</a>
</li>
<li class="py-2">
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Characters">Meet the Characters</a>
</li>

View File

@ -0,0 +1,175 @@
@{
ViewData["Title"] = "The Alpha Flame: Story Map";
var mapImg = Url.Content("~/images/the-alpha-flame-map.png"); /* update path */
}
<section class="py-2">
<div class="container">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-3">
<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">Story Map</li>
</ol>
</nav>
<!-- Title + intro -->
<div class="row mb-3">
<div class="col">
<h1 class="mb-2">The Alpha Flame: <span class="fw-light">Story Map</span></h1>
<p class="lead mb-0">
A hand-drawn guide to the Midlands setting of <em>The Alpha Flame: Discovery</em>, sketched to show how the key places relate in 1983. Schematic, not to scale.
</p>
</div>
</div>
<!-- Responsive layout: map left on desktop, stacked on mobile -->
<div class="row g-4">
<!-- LEFT: Map -->
<div class="col-12 col-lg-6 order-1 order-lg-1">
<figure class="card shadow-sm map-sticky">
<responsive-image src="the-alpha-flame-map.png" alt="Hand-drawn notepaper map showing Rubery, Frankley, Lickey Hills, Barnt Green, Redditch, and key landmarks from The Alpha Flame" class="card-img-top img-fluid" display-width-percentage="50"></responsive-image>
<figcaption class="card-body small text-muted">
Hand-drawn story map of the 1983 Midlands setting. Locations and routes are indicative for reader orientation.
</figcaption>
</figure>
</div>
<!-- RIGHT: Location blurbs -->
<div class="col-12 col-lg-6 order-2 order-lg-2">
<div class="row row-cols-1 g-3">
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Cock Hill Lane</h2>
<p class="card-text">A steep lane rising from Rubery, close to daily life. Streets and side roads here frame several quiet, telling moments.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">The Doweries & Deelands Road</h2>
<p class="card-text">Neighbouring estates near Cock Hill Lane that capture the feel of working-class Birmingham in the early eighties.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Rubery Hill Hospital</h2>
<p class="card-text">A former psychiatric hospital overlooking the area. Derelict by the period of the book, it lends an eerie weight to the landscape.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Frankley & Reservoir</h2>
<p class="card-text">Beths childhood backdrop. The water and open ground carry memories, choices, and the starkness of survival.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Lickey Hills</h2>
<p class="card-text">Green relief between Rubery and Barnt Green. A pocket of space where characters pause, breathe, and gain perspective.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Barnt Green & The Inn</h2>
<p class="card-text">A village south of Rubery that feels a world apart. The Barnt Green Inn becomes a place for gathering and reflection.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Redditch: Limelight & Washford Mill</h2>
<p class="card-text">Nightlife and edge. The nightclub and riverside pub offer contrast to the quieter streets nearer home.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Belbroughton: The Bell</h2>
<p class="card-text">A country pub to the west. Distance and quiet lanes give scenes here a different texture.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Austin Longbridge</h2>
<p class="card-text">The car plant that symbolised Birminghams industrial heartbeat. A landmark as much as an employer.</p>
</div>
</div>
</div>
<div class="col">
<div class="card h-100">
<div class="card-body">
<h2 class="h5 card-title">Birmingham</h2>
<p class="card-text">The city on the horizon. Bigger, busier, sometimes darker, always shaping the choices nearby.</p>
</div>
</div>
</div>
</div>
<p class="text-muted small mt-3 mb-0">
Artistic interpretation of 1983 locations; relationships over precise distances.
</p>
</div>
</div>
</div>
</section>
<!-- Optional: SEO structured data -->
<script type="application/ld+json">
{
"@@context": "https://schema.org",
"@@type": "WebPage",
"name": "The Alpha Flame: Story Map",
"description": "Hand-drawn story map of the Midlands setting for The Alpha Flame: Discovery with short descriptions of key locations.",
"image": {
"@@type": "ImageObject",
"url": "@mapImg",
"caption": "The Alpha Flame hand-drawn story map"
},
"about": {
"@@type": "ItemList",
"itemListElement": [
{"@@type": "ListItem","position": 1,"name": "Cock Hill Lane"},
{"@@type": "ListItem","position": 2,"name": "The Doweries"},
{"@@type": "ListItem","position": 3,"name": "Deelands Road"},
{"@@type": "ListItem","position": 4,"name": "Rubery Hill Hospital"},
{"@@type": "ListItem","position": 5,"name": "Frankley & Reservoir"},
{"@@type": "ListItem","position": 6,"name": "Lickey Hills"},
{"@@type": "ListItem","position": 7,"name": "Barnt Green & The Inn"},
{"@@type": "ListItem","position": 8,"name": "Redditch: Limelight & Washford Mill"},
{"@@type": "ListItem","position": 9,"name": "Belbroughton: The Bell"},
{"@@type": "ListItem","position": 10,"name": "Austin Longbridge"},
{"@@type": "ListItem","position": 11,"name": "Birmingham"}
]
}
}
</script>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
const CACHE_NAME = 'catherine-lynwood-v8';
const CACHE_NAME = 'catherine-lynwood-v9';
const urlsToCache = [
'/',
'/manifest.json',