Compare commits
10 Commits
b4a3611711
...
85924f63d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85924f63d3 | ||
|
|
fb4e139d9f | ||
|
|
8215d83f63 | ||
|
|
3ce32496e8 | ||
|
|
4759fbbd69 | ||
|
|
b3cc5ccedd | ||
|
|
82cb5cde02 | ||
|
|
afe3c6cc78 | ||
|
|
31ae1e80ea | ||
|
|
f0e5052e2c |
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@ -153,19 +153,19 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
|
||||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
<PackageReference Include="MailKit" Version="4.15.1" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Web.Administration" Version="11.1.0" />
|
<PackageReference Include="Microsoft.Web.Administration" Version="11.1.0" />
|
||||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
<PackageReference Include="NAudio" Version="2.3.0" />
|
||||||
<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.Fonts" Version="2.1.3" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.1.5" />
|
<PackageReference Include="SixLabors.ImageSharp.Web" Version="3.2.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.5" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="WebMarkupMin.AspNetCoreLatest" Version="2.19.0" />
|
<PackageReference Include="WebMarkupMin.AspNetCoreLatest" Version="2.21.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
41
CatherineLynwood/Components/BuyPanel.cs
Normal file
41
CatherineLynwood/Components/BuyPanel.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Components
|
||||||
|
{
|
||||||
|
public class BuyPanel : ViewComponent
|
||||||
|
{
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
|
private DataAccess _dataAccess;
|
||||||
|
|
||||||
|
#endregion Private Fields
|
||||||
|
|
||||||
|
#region Public Constructors
|
||||||
|
|
||||||
|
public BuyPanel(DataAccess dataAccess)
|
||||||
|
{
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Public Constructors
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
public async Task<IViewComponentResult> InvokeAsync(string iso2, string src, string title)
|
||||||
|
{
|
||||||
|
BuyPanelViewModel buyPanelViewModel = await _dataAccess.GetBuyPanelViewModel(iso2, title);
|
||||||
|
buyPanelViewModel.Src = src;
|
||||||
|
|
||||||
|
if (title == "Reckoning")
|
||||||
|
{
|
||||||
|
return View("Reckoning", buyPanelViewModel);
|
||||||
|
}
|
||||||
|
return View(buyPanelViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Public Methods
|
||||||
|
}
|
||||||
|
}
|
||||||
95
CatherineLynwood/Controllers/ClicksController.cs
Normal file
95
CatherineLynwood/Controllers/ClicksController.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public ClicksController(DataAccess dataAccess, ICountryContext country)
|
||||||
|
{
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
_country = country;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// POST /track/click?slug=amazon-paperback-gb&src=discovery
|
||||||
|
// Receives <a ping="..."> pings and records them (204 No Content).
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
[HttpPost("track/click")]
|
||||||
|
public async Task<IActionResult> TrackClick([FromQuery] string slug, [FromQuery] string src = "discovery")
|
||||||
|
{
|
||||||
|
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";
|
||||||
|
|
||||||
|
if (HttpMethods.IsHead(Request.Method)) logClick = false; // don't log HEAD requests
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
private void EnsureSidCookie(out string sessionId)
|
||||||
|
{
|
||||||
|
sessionId = Request.Cookies["sid"]!;
|
||||||
|
if (!string.IsNullOrEmpty(sessionId)) return;
|
||||||
|
|
||||||
|
sessionId = Guid.NewGuid().ToString("N");
|
||||||
|
Response.Cookies.Append("sid", sessionId, new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Secure = true,
|
||||||
|
MaxAge = TimeSpan.FromDays(365),
|
||||||
|
Path = "/"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLikelyBot(string ua)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ua)) return true; // empty UA, treat as bot
|
||||||
|
var needles = new[]
|
||||||
|
{
|
||||||
|
"bot","crawler","spider","preview","fetch","scan","analyzer",
|
||||||
|
"httpclient","python-requests","facebookexternalhit","slackbot",
|
||||||
|
"whatsapp","telegrambot","googlebot","adsbot","amazonbot",
|
||||||
|
"bingbot","duckduckbot","yandexbot","ahrefsbot","semrushbot",
|
||||||
|
"applebot","linkedinbot","discordbot","embedly","pinterestbot"
|
||||||
|
};
|
||||||
|
ua = ua.ToLowerInvariant();
|
||||||
|
return needles.Any(n => ua.Contains(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace CatherineLynwood.Controllers
|
namespace CatherineLynwood.Controllers
|
||||||
@ -13,97 +14,277 @@ namespace CatherineLynwood.Controllers
|
|||||||
[Route("the-alpha-flame/discovery")]
|
[Route("the-alpha-flame/discovery")]
|
||||||
public class DiscoveryController : Controller
|
public class DiscoveryController : Controller
|
||||||
{
|
{
|
||||||
#region Private Fields
|
private readonly DataAccess _dataAccess;
|
||||||
|
private readonly ICountryContext _country;
|
||||||
|
|
||||||
private DataAccess _dataAccess;
|
// A/B/C constants
|
||||||
|
private const string VariantCookie = "af_disc_abc";
|
||||||
|
private const string VariantQuery = "ab"; // ?ab=A or B or C
|
||||||
|
|
||||||
#endregion Private Fields
|
public DiscoveryController(DataAccess dataAccess, ICountryContext country)
|
||||||
|
|
||||||
#region Public Constructors
|
|
||||||
|
|
||||||
public DiscoveryController(DataAccess dataAccess)
|
|
||||||
{
|
{
|
||||||
_dataAccess = dataAccess;
|
_dataAccess = dataAccess;
|
||||||
}
|
_country = country;
|
||||||
|
|
||||||
#endregion Public Constructors
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
|
|
||||||
[Route("chapters/chapter-1-beth")]
|
|
||||||
public IActionResult Chapter1()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("chapters/chapter-13-susie")]
|
|
||||||
public IActionResult Chapter13()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("chapters/chapter-2-maggie")]
|
|
||||||
public IActionResult Chapter2()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[BookAccess(1, 1)]
|
|
||||||
[Route("extras/epilogue")]
|
|
||||||
public IActionResult Epilogue()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[BookAccess(1, 1)]
|
|
||||||
[Route("extras")]
|
|
||||||
public IActionResult Extras()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("")]
|
[Route("")]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
Reviews reviews = await _dataAccess.GetReviewsAsync();
|
// Prevent caches or CDNs from cross-serving variants
|
||||||
|
Response.Headers["Vary"] = "Cookie, User-Agent";
|
||||||
|
|
||||||
|
// Decide device class first
|
||||||
|
var device = ResolveDeviceClass();
|
||||||
|
|
||||||
|
// Decide variant
|
||||||
|
var variant = ResolveVariant(device);
|
||||||
|
|
||||||
|
// Country ISO2
|
||||||
|
var iso2 = (_country.Iso2 ?? "GB").ToUpperInvariant();
|
||||||
|
if (iso2 == "UK") iso2 = "GB";
|
||||||
|
|
||||||
|
// Reviews
|
||||||
|
Reviews reviews = await _dataAccess.GetReviewsAsync("Discovery");
|
||||||
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
|
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
|
||||||
|
|
||||||
return View(reviews);
|
string src = variant switch
|
||||||
|
{
|
||||||
|
Variant.A => "DiscoveryA",
|
||||||
|
Variant.B => "DiscoveryA",
|
||||||
|
_ => "Desktop"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// VM
|
||||||
|
var vm = new TitlePageViewModel
|
||||||
|
{
|
||||||
|
Reviews = reviews,
|
||||||
|
UserIso2 = iso2,
|
||||||
|
Src = src,
|
||||||
|
Title = "Discovery"
|
||||||
|
};
|
||||||
|
|
||||||
|
// View mapping:
|
||||||
|
// - A and B are the two MOBILE variants
|
||||||
|
// - C is the DESKTOP variant
|
||||||
|
string viewName = variant switch
|
||||||
|
{
|
||||||
|
Variant.A => "IndexMobileA", // mobile layout A
|
||||||
|
Variant.B => "IndexMobileA", // mobile layout B
|
||||||
|
_ => "IndexDesktop" // desktop layout C
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(viewName, vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("reviews")]
|
[Route("reviews")]
|
||||||
public async Task<IActionResult> Reviews()
|
public async Task<IActionResult> Reviews()
|
||||||
{
|
{
|
||||||
Reviews reviews = await _dataAccess.GetReviewsAsync();
|
Reviews reviews = await _dataAccess.GetReviewsAsync("Discovery");
|
||||||
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100);
|
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100);
|
||||||
|
|
||||||
return View(reviews);
|
return View(reviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BookAccess(1, 1)]
|
[Route("audio-book")]
|
||||||
[Route("extras/listen")]
|
public IActionResult AudioBook() => View();
|
||||||
public IActionResult Listen()
|
|
||||||
|
[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()
|
||||||
{
|
{
|
||||||
return View();
|
// 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)]
|
[BookAccess(1, 1)]
|
||||||
[Route("extras/maggies-designs")]
|
[Route("extras")]
|
||||||
public IActionResult MaggiesDesigns()
|
public IActionResult Extras() => View();
|
||||||
{
|
|
||||||
return View();
|
[BookAccess(1, 1)]
|
||||||
}
|
[Route("extras/epilogue")]
|
||||||
|
public IActionResult Epilogue() => View();
|
||||||
|
|
||||||
[BookAccess(1, 1)]
|
[BookAccess(1, 1)]
|
||||||
[Route("extras/scrap-book")]
|
[Route("extras/scrap-book")]
|
||||||
public IActionResult ScrapBook()
|
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()
|
||||||
{
|
{
|
||||||
return View();
|
List<SoundtrackTrackModel> soundtrackTrackModels = await _dataAccess.GetSoundtrack();
|
||||||
|
return View(soundtrackTrackModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Methods
|
// =========================
|
||||||
|
// A/B/C selection
|
||||||
|
// =========================
|
||||||
|
|
||||||
#region Private Methods
|
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)
|
private string GenerateBookSchemaJsonLd(Reviews reviews, int take)
|
||||||
{
|
{
|
||||||
@ -129,22 +310,20 @@ namespace CatherineLynwood.Controllers
|
|||||||
["name"] = "Catherine Lynwood"
|
["name"] = "Catherine Lynwood"
|
||||||
},
|
},
|
||||||
["datePublished"] = "2025-08-21",
|
["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...",
|
["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",
|
["genre"] = "Women's Fiction, Mystery, Contemporary Historical",
|
||||||
["inLanguage"] = "en-GB",
|
["inLanguage"] = "en-GB",
|
||||||
["url"] = baseUrl
|
["url"] = baseUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add review section if there are reviews
|
|
||||||
if (reviews?.Items?.Any() == true)
|
if (reviews?.Items?.Any() == true)
|
||||||
{
|
{
|
||||||
var reviewObjects = new List<Dictionary<string, object>>();
|
var reviewObjects = new List<Dictionary<string, object>>();
|
||||||
double total = 0;
|
double total = 0;
|
||||||
|
foreach (var review in reviews.Items) total += review.RatingValue;
|
||||||
|
|
||||||
foreach (var review in reviews.Items.Take(take))
|
foreach (var review in reviews.Items.Take(take))
|
||||||
{
|
{
|
||||||
total += review.RatingValue;
|
|
||||||
|
|
||||||
reviewObjects.Add(new Dictionary<string, object>
|
reviewObjects.Add(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["@type"] = "Review",
|
["@type"] = "Review",
|
||||||
@ -173,7 +352,6 @@ namespace CatherineLynwood.Controllers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add work examples
|
|
||||||
schema["workExample"] = new List<Dictionary<string, object>>
|
schema["workExample"] = new List<Dictionary<string, object>>
|
||||||
{
|
{
|
||||||
new Dictionary<string, object>
|
new Dictionary<string, object>
|
||||||
@ -245,9 +423,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
return JsonConvert.SerializeObject(schema, Formatting.Indented);
|
return JsonConvert.SerializeObject(schema, Formatting.Indented);
|
||||||
}
|
}
|
||||||
|
|
||||||
string StripHtml(string input) => string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty);
|
private static string StripHtml(string input) =>
|
||||||
|
string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty);
|
||||||
|
|
||||||
#endregion Private Methods
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using CatherineLynwood.Models;
|
using CatherineLynwood.Models;
|
||||||
using CatherineLynwood.Services;
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -40,16 +40,39 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View(arcReaderApplicationModel);
|
return View(arcReaderApplicationModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("arc-reader-application/thanks")]
|
||||||
|
public IActionResult ArcThanks()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("arc-reader-application")]
|
[HttpPost("arc-reader-application")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> ArcReaderApplication(ArcReaderApplicationModel arcReaderApplicationModel)
|
public async Task<IActionResult> ArcReaderApplication(ArcReaderApplicationModel arcReaderApplicationModel)
|
||||||
{
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return View(arcReaderApplicationModel);
|
||||||
|
}
|
||||||
|
|
||||||
bool success = await _dataAccess.SaveARCReaderApplication(arcReaderApplicationModel);
|
bool success = await _dataAccess.SaveARCReaderApplication(arcReaderApplicationModel);
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
return RedirectToAction("ThankYou");
|
if (arcReaderApplicationModel.HasKindleAccess == "No")
|
||||||
|
{
|
||||||
|
await SendARCAltDeliveryEmailAsync(arcReaderApplicationModel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SendARCInstructionsEmailAsync(arcReaderApplicationModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("ArcThanks");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If saving fails unexpectedly
|
||||||
|
ModelState.AddModelError("", "There was an error submitting your application. Please try again later.");
|
||||||
return View(arcReaderApplicationModel);
|
return View(arcReaderApplicationModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +99,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
{
|
{
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
await _dataAccess.SaveContact(contact);
|
await _dataAccess.SaveContact(contact, false);
|
||||||
|
|
||||||
var subject = "Email from Catherine Lynwood Web Site";
|
var subject = "Email from Catherine Lynwood Web Site";
|
||||||
var plainTextContent = $"Email from: {contact.Name} ({contact.EmailAddress})\r\n{contact.Message}";
|
var plainTextContent = $"Email from: {contact.Name} ({contact.EmailAddress})\r\n{contact.Message}";
|
||||||
@ -193,8 +216,13 @@ namespace CatherineLynwood.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Route("verostic-genre")]
|
[Route("verostic-genre")]
|
||||||
public IActionResult VerosticGenre()
|
public IActionResult VerosticGenre(int step)
|
||||||
{
|
{
|
||||||
|
if (step > 0)
|
||||||
|
{
|
||||||
|
ViewData["step"] = step;
|
||||||
|
}
|
||||||
|
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,6 +259,83 @@ namespace CatherineLynwood.Controllers
|
|||||||
userAgent.Contains("ipad");
|
userAgent.Contains("ipad");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendARCInstructionsEmailAsync(ArcReaderApplicationModel arc)
|
||||||
|
{
|
||||||
|
var subject = "Your ARC Copy of *The Alpha Flame: Discovery* – Kindle Instructions";
|
||||||
|
|
||||||
|
var plainTextContent = $@"Hi {arc.FullName},
|
||||||
|
|
||||||
|
Thank you so much for applying to be an ARC reader! I’m thrilled to have you on board.
|
||||||
|
|
||||||
|
Since you selected Kindle delivery, here’s what you need to do next:
|
||||||
|
|
||||||
|
1. Add my sender email address to your approved Kindle senders list: catherine@catherinelynwood.com
|
||||||
|
2. Make sure you’ve added your correct Kindle email: {arc.KindleEmail}@kindle.com
|
||||||
|
|
||||||
|
You can do this by visiting https://www.amazon.co.uk/myk → Preferences → Personal Document Settings.
|
||||||
|
|
||||||
|
Once this is done let me know and then I’ll be sending your ARC shortly!
|
||||||
|
|
||||||
|
All my best,
|
||||||
|
Catherine";
|
||||||
|
|
||||||
|
var htmlContent = $@"
|
||||||
|
<p>Hi {arc.FullName},</p>
|
||||||
|
<p>Thank you so much for applying to be an ARC reader! I’m thrilled to have you on board.</p>
|
||||||
|
<p>Since you selected <strong>Kindle delivery</strong>, here’s what you need to do next:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Add my sender email address to your approved Kindle senders list: <strong>catherine@catherinelynwood.com</strong></li>
|
||||||
|
<li>Ensure your Kindle email is correct: <strong>{arc.KindleEmail}@kindle.com</strong></li>
|
||||||
|
</ol>
|
||||||
|
<p>You can find these settings at <a href='https://www.amazon.co.uk/myk' target='_blank'>amazon.co.uk/myk</a> → Preferences → Personal Document Settings.</p>
|
||||||
|
<p>Once that’s set up, let me know and I’ll send your ARC soon!</p>
|
||||||
|
<p>Warmly,<br/>Catherine</p>";
|
||||||
|
|
||||||
|
Contact contact = new Contact
|
||||||
|
{
|
||||||
|
Name = arc.FullName,
|
||||||
|
EmailAddress = arc.Email
|
||||||
|
};
|
||||||
|
|
||||||
|
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendARCAltDeliveryEmailAsync(ArcReaderApplicationModel arc)
|
||||||
|
{
|
||||||
|
var subject = "Your ARC Copy of *The Alpha Flame: Discovery* – Next Steps";
|
||||||
|
|
||||||
|
var plainTextContent = $@"Hi {arc.FullName},
|
||||||
|
|
||||||
|
Thank you for applying to be an ARC reader – I really appreciate it!
|
||||||
|
|
||||||
|
I noticed you selected an alternative to Kindle. That’s absolutely fine.
|
||||||
|
|
||||||
|
I’ll be in touch shortly to offer a secure way to get your copy – one that’s simple and keeps the book protected from piracy.
|
||||||
|
|
||||||
|
If you have any questions or preferred reading methods (like phone, tablet, computer), feel free to reply to this email.
|
||||||
|
|
||||||
|
Thank you again,
|
||||||
|
Catherine";
|
||||||
|
|
||||||
|
var htmlContent = $@"
|
||||||
|
<p>Hi {arc.FullName},</p>
|
||||||
|
<p>Thank you for applying to be an ARC reader – I really appreciate it!</p>
|
||||||
|
<p>I noticed you selected an alternative to Kindle. That’s absolutely fine.</p>
|
||||||
|
<p>I’ll be in touch shortly to offer a secure way to get your copy – one that’s simple and keeps the book protected from piracy.</p>
|
||||||
|
<p>If you have any questions or preferred reading methods (like phone, tablet, or computer), just reply to this email.</p>
|
||||||
|
<p>Thanks again,<br/>Catherine</p>";
|
||||||
|
|
||||||
|
Contact contact = new Contact
|
||||||
|
{
|
||||||
|
Name = arc.FullName,
|
||||||
|
EmailAddress = arc.Email
|
||||||
|
};
|
||||||
|
|
||||||
|
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#endregion Private Methods
|
#endregion Private Methods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
86
CatherineLynwood/Controllers/IndieAuthorController.cs
Normal file
86
CatherineLynwood/Controllers/IndieAuthorController.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Controllers
|
||||||
|
{
|
||||||
|
[Route("indie-author")]
|
||||||
|
public class IndieAuthorController : Controller
|
||||||
|
{
|
||||||
|
[HttpGet("")]
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("isbns")]
|
||||||
|
public IActionResult Isbns()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("kdp")]
|
||||||
|
public IActionResult Kdp()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("audiobooks")]
|
||||||
|
public IActionResult Audiobooks()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("ingramspark")]
|
||||||
|
public IActionResult IngramSpark()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("kdp-vs-ingramspark")]
|
||||||
|
public IActionResult KdpVsIngramSpark()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("editing-and-proofreading")]
|
||||||
|
public IActionResult EditingAndProofreading()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("amazon-advertising")]
|
||||||
|
public IActionResult AmazonAdvertising()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("reviews-and-arc-readers")]
|
||||||
|
public IActionResult ReviewsAndArcReaders()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("cover-design")]
|
||||||
|
public IActionResult CoverDesign()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("ai-for-authors")]
|
||||||
|
public IActionResult AiForAuthors()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("traditional-vs-self-publishing")]
|
||||||
|
public IActionResult TraditionalVsSelfPublishing()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("author-finances")]
|
||||||
|
public IActionResult AuthorFinances()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
426
CatherineLynwood/Controllers/ReckoningController.cs
Normal file
426
CatherineLynwood/Controllers/ReckoningController.cs
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
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()
|
||||||
|
{
|
||||||
|
// Prevent caches or CDNs from cross-serving variants
|
||||||
|
Response.Headers["Vary"] = "Cookie, User-Agent";
|
||||||
|
|
||||||
|
// Decide device class first
|
||||||
|
var device = ResolveDeviceClass();
|
||||||
|
|
||||||
|
// Decide variant
|
||||||
|
var variant = ResolveVariant(device);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
string src = variant switch
|
||||||
|
{
|
||||||
|
Variant.A => "Mobile",
|
||||||
|
Variant.B => "Mobile",
|
||||||
|
_ => "Desktop"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// VM
|
||||||
|
var vm = new TitlePageViewModel
|
||||||
|
{
|
||||||
|
Reviews = reviews,
|
||||||
|
UserIso2 = iso2,
|
||||||
|
Src = src,
|
||||||
|
Title = "Reckoning"
|
||||||
|
};
|
||||||
|
|
||||||
|
// View mapping:
|
||||||
|
// - A and B are the two MOBILE variants
|
||||||
|
// - C is the DESKTOP variant
|
||||||
|
string viewName = variant switch
|
||||||
|
{
|
||||||
|
Variant.A => "IndexMobile", // mobile layout A
|
||||||
|
Variant.B => "IndexMobile", // mobile layout B
|
||||||
|
_ => "Index" // desktop layout C
|
||||||
|
};
|
||||||
|
|
||||||
|
return View(viewName, 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, 2)]
|
||||||
|
[Route("extras")]
|
||||||
|
public IActionResult Extras() => View();
|
||||||
|
|
||||||
|
[BookAccess(1, 2)]
|
||||||
|
[Route("extras/epilogue")]
|
||||||
|
public IActionResult Epilogue() => View();
|
||||||
|
|
||||||
|
[BookAccess(1, 2)]
|
||||||
|
[Route("extras/scrap-book")]
|
||||||
|
public IActionResult ScrapBook() => View();
|
||||||
|
|
||||||
|
[BookAccess(1, 2)]
|
||||||
|
[Route("extras/listen")]
|
||||||
|
public IActionResult Listen() => View();
|
||||||
|
|
||||||
|
[BookAccess(1, 2)]
|
||||||
|
[Route("extras/maggies-designs")]
|
||||||
|
public IActionResult MaggiesDesigns() => View();
|
||||||
|
|
||||||
|
[BookAccess(1, 2)]
|
||||||
|
[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,18 +38,37 @@ namespace CatherineLynwood.Controllers
|
|||||||
new SitemapEntry { Url = Url.Action("Chapter1", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
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("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("Chapter2", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
new SitemapEntry { Url = Url.Action("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
new SitemapEntry { Url = Url.Action("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("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("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("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", "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", "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", "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", "Publishing", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
new SitemapEntry { Url = Url.Action("Privacy", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
new SitemapEntry { Url = Url.Action("Privacy", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
new SitemapEntry { Url = Url.Action("SamanthaLynwood", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
new SitemapEntry { Url = Url.Action("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("VerosticGenre", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("LarhysaSaddul", "Collaborations", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Index", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Isbn", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Kdp", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Audiobooks", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("IngramSpark", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("KdpVsIngramSpark", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("EditingAndProofreading", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("AmazonAdvertising", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("ReviewsAndArcReaders", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("TraditionalVsSelfPublishing", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("AuthorFinances", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("CoverDesign", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("AiForAuthors", "IndieAuthor", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
// Additional static pages
|
// Additional static pages
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
68
CatherineLynwood/Controllers/SupportController.cs
Normal file
68
CatherineLynwood/Controllers/SupportController.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Controllers
|
||||||
|
{
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/support")]
|
||||||
|
public class SupportController : ControllerBase
|
||||||
|
{
|
||||||
|
private DataAccess _dataAccess;
|
||||||
|
|
||||||
|
public record FlagDto(string Country);
|
||||||
|
|
||||||
|
public class SubscriptionDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SupportController(DataAccess dataAccess)
|
||||||
|
{
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("flag")]
|
||||||
|
public async Task<IActionResult> Flag([FromBody] FlagDto dto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dto?.Country))
|
||||||
|
return BadRequest(new { ok = false, error = "Country required" });
|
||||||
|
|
||||||
|
// TODO: replace with real DB call
|
||||||
|
var total = await _dataAccess.SaveFlagClick(dto.Country);
|
||||||
|
|
||||||
|
return Ok(new { ok = true, total });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("subscribe")]
|
||||||
|
public IActionResult Subscribe([FromBody] SubscriptionDto dto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dto?.Email))
|
||||||
|
return BadRequest(new { ok = false, error = "Email required" });
|
||||||
|
|
||||||
|
Contact contact = new Contact
|
||||||
|
{
|
||||||
|
EmailAddress = dto.Email,
|
||||||
|
Name = dto.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
var ok = _dataAccess.SaveContact(contact, true);
|
||||||
|
|
||||||
|
return Ok(new { ok });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Dummy persistence you can swap for EF/Dapper calls -----
|
||||||
|
|
||||||
|
|
||||||
|
private static bool SaveSubscription(string email, string country)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,7 @@
|
|||||||
using CatherineLynwood.Helpers;
|
using CatherineLynwood.Models;
|
||||||
using CatherineLynwood.Models;
|
|
||||||
using CatherineLynwood.Services;
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Identity.Client;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@ -52,6 +50,14 @@ namespace CatherineLynwood.Controllers
|
|||||||
blogIndex.BlogFilter.TotalPages = (int)Math.Ceiling((double)blogIndex.Blogs.Count / blogFilter.ResultsPerPage);
|
blogIndex.BlogFilter.TotalPages = (int)Math.Ceiling((double)blogIndex.Blogs.Count / blogFilter.ResultsPerPage);
|
||||||
blogIndex.IsMobile = IsMobile(Request);
|
blogIndex.IsMobile = IsMobile(Request);
|
||||||
|
|
||||||
|
// Add this check before paginating
|
||||||
|
if (blogIndex.BlogFilter.PageNumber > blogIndex.BlogFilter.TotalPages && blogIndex.BlogFilter.TotalPages > 0)
|
||||||
|
{
|
||||||
|
Response.StatusCode = 404;
|
||||||
|
return View("BlogNotFound");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
if (blogFilter.SortDirection == 1)
|
if (blogFilter.SortDirection == 1)
|
||||||
blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList();
|
blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList();
|
||||||
@ -149,6 +155,30 @@ namespace CatherineLynwood.Controllers
|
|||||||
return RedirectToAction("BlogItem", new { slug = blogUrl, showThanks = showThanks });
|
return RedirectToAction("BlogItem", new { slug = blogUrl, showThanks = showThanks });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BookAccess(1)]
|
||||||
|
[Route("extras")]
|
||||||
|
public IActionResult Extras()
|
||||||
|
{
|
||||||
|
var session = HttpContext.Session;
|
||||||
|
|
||||||
|
var level = session.GetInt32("BookAccessLevel");
|
||||||
|
var book = session.GetInt32("BookAccessMax");
|
||||||
|
|
||||||
|
if (book == 2)
|
||||||
|
{
|
||||||
|
return RedirectToAction("Extras", "Reckoning");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (book == 3)
|
||||||
|
{
|
||||||
|
return RedirectToAction("Extras", "Redemption");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return RedirectToAction("Extras", "Discovery");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[Route("")]
|
[Route("")]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
@ -206,11 +236,6 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("reckoning")]
|
|
||||||
public IActionResult Reckoning()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Route("redemption")]
|
[Route("redemption")]
|
||||||
public IActionResult Redemption()
|
public IActionResult Redemption()
|
||||||
@ -248,6 +273,12 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("story-map")]
|
||||||
|
public IActionResult StoryMap()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Public Methods
|
#endregion Public Methods
|
||||||
|
|
||||||
#region Private Methods
|
#region Private Methods
|
||||||
|
|||||||
@ -6,7 +6,7 @@ public class BookAccessAttribute : Attribute, IAsyncAuthorizationFilter
|
|||||||
private readonly int _requiredLevel;
|
private readonly int _requiredLevel;
|
||||||
private readonly int _requiredBook;
|
private readonly int _requiredBook;
|
||||||
|
|
||||||
public BookAccessAttribute(int requiredLevel, int requiredBook)
|
public BookAccessAttribute(int requiredLevel, int requiredBook = 0)
|
||||||
{
|
{
|
||||||
_requiredLevel = requiredLevel;
|
_requiredLevel = requiredLevel;
|
||||||
_requiredBook = requiredBook;
|
_requiredBook = requiredBook;
|
||||||
@ -19,13 +19,28 @@ public class BookAccessAttribute : Attribute, IAsyncAuthorizationFilter
|
|||||||
var level = session.GetInt32("BookAccessLevel");
|
var level = session.GetInt32("BookAccessLevel");
|
||||||
var book = session.GetInt32("BookAccessMax");
|
var book = session.GetInt32("BookAccessMax");
|
||||||
|
|
||||||
if (level == null || book == null || level < _requiredLevel || book < _requiredBook)
|
if (_requiredBook == 0)
|
||||||
{
|
{
|
||||||
var currentUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString;
|
if (level == null || book == null || level < _requiredLevel)
|
||||||
context.HttpContext.Items["RequestedUrl"] = currentUrl; // store temporarily
|
{
|
||||||
|
var currentUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString;
|
||||||
|
context.HttpContext.Items["RequestedUrl"] = currentUrl; // store temporarily
|
||||||
|
|
||||||
context.Result = new RedirectToActionResult("Prompt", "Access", new { returnUrl = currentUrl });
|
context.Result = new RedirectToActionResult("Prompt", "Access", new { returnUrl = currentUrl });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (level == null || book == null || level < _requiredLevel || book != _requiredBook)
|
||||||
|
{
|
||||||
|
var currentUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString;
|
||||||
|
context.HttpContext.Items["RequestedUrl"] = currentUrl; // store temporarily
|
||||||
|
|
||||||
|
context.Result = new RedirectToActionResult("Prompt", "Access", new { returnUrl = currentUrl });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
CatherineLynwood/Middleware/EnsureSidMiddleware.cs
Normal file
35
CatherineLynwood/Middleware/EnsureSidMiddleware.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
namespace CatherineLynwood.Middleware
|
||||||
|
{
|
||||||
|
public class EnsureSidMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
public EnsureSidMiddleware(RequestDelegate next) => _next = next;
|
||||||
|
|
||||||
|
public async Task Invoke(HttpContext ctx)
|
||||||
|
{
|
||||||
|
if (!ctx.Request.Cookies.ContainsKey("sid") &&
|
||||||
|
string.Equals(ctx.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var accept = ctx.Request.Headers["Accept"].ToString();
|
||||||
|
var fetchMode = ctx.Request.Headers["Sec-Fetch-Mode"].ToString();
|
||||||
|
|
||||||
|
// Only issue on real navigations to HTML
|
||||||
|
if (accept.Contains("text/html", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(fetchMode, "navigate", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ctx.Response.Cookies.Append("sid", Guid.NewGuid().ToString("N"), new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Secure = true,
|
||||||
|
MaxAge = TimeSpan.FromDays(365),
|
||||||
|
Path = "/"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
115
CatherineLynwood/Middleware/GeoResolutionMiddleware.cs
Normal file
115
CatherineLynwood/Middleware/GeoResolutionMiddleware.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Middleware
|
||||||
|
{
|
||||||
|
|
||||||
|
public sealed class GeoResolutionMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IGeoResolver _resolver;
|
||||||
|
private readonly ILogger<GeoResolutionMiddleware> _logger;
|
||||||
|
private readonly IHostEnvironment _env;
|
||||||
|
|
||||||
|
public GeoResolutionMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IGeoResolver resolver,
|
||||||
|
ILogger<GeoResolutionMiddleware> logger,
|
||||||
|
IHostEnvironment env)
|
||||||
|
{
|
||||||
|
_next = next; _resolver = resolver; _logger = logger; _env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("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);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("GeoMW: loopback or null IP in non-dev; skipping");
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("GeoMW: calling resolver for {IP}", ip);
|
||||||
|
|
||||||
|
var geo = await _resolver.ResolveAsync(ip, path, queryString, userAgent);
|
||||||
|
|
||||||
|
if (geo is not null)
|
||||||
|
{
|
||||||
|
context.Items[HttpContextItemKeys.CountryIso2] = geo.Iso2;
|
||||||
|
context.Items[HttpContextItemKeys.CountryName] = geo.CountryName ?? "";
|
||||||
|
context.Response.Headers["X-Geo-Iso2"] = geo.Iso2; // debug aid
|
||||||
|
_logger.LogInformation("GeoMW: resolved {IP} -> {ISO2} ({Country})", ip, geo.Iso2, geo.CountryName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.Headers["X-Geo-Iso2"] = "none"; // debug aid
|
||||||
|
_logger.LogWarning("GeoMW: resolver returned null for {IP}", ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPAddress? GetClientIp(HttpContext ctx)
|
||||||
|
{
|
||||||
|
// Prefer X-Forwarded-For when present
|
||||||
|
if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff))
|
||||||
|
{
|
||||||
|
var first = xff.ToString().Split(',').Select(s => s.Trim()).FirstOrDefault();
|
||||||
|
if (IPAddress.TryParse(first, out var parsed)) return parsed;
|
||||||
|
}
|
||||||
|
return ctx.Connection.RemoteIpAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -61,10 +61,10 @@ namespace CatherineLynwood.Middleware
|
|||||||
//
|
//
|
||||||
if (path.EndsWith(".php", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".env", StringComparison.OrdinalIgnoreCase))
|
if (path.EndsWith(".php", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".env", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
//if (!_environment.IsDevelopment() && ipAddress != null)
|
if (!_environment.IsDevelopment() && ipAddress != null)
|
||||||
//{
|
{
|
||||||
// TryBlockIpInIIS(ipAddress);
|
TryBlockIpInIIS(ipAddress);
|
||||||
//}
|
}
|
||||||
|
|
||||||
_logger.LogWarning("Blocked .php/.env probe from {IP}: {Path}", ipAddress, path);
|
_logger.LogWarning("Blocked .php/.env probe from {IP}: {Path}", ipAddress, path);
|
||||||
|
|
||||||
@ -98,10 +98,10 @@ namespace CatherineLynwood.Middleware
|
|||||||
// Only block if referer is present but NOT in the whitelist
|
// Only block if referer is present but NOT in the whitelist
|
||||||
if (!string.IsNullOrEmpty(referer) && !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.Ordinal)))
|
if (!string.IsNullOrEmpty(referer) && !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
//if (!_environment.IsDevelopment() && ipAddress != null)
|
if (!_environment.IsDevelopment() && ipAddress != null)
|
||||||
//{
|
{
|
||||||
// TryBlockIpInIIS(ipAddress);
|
TryBlockIpInIIS(ipAddress);
|
||||||
//}
|
}
|
||||||
|
|
||||||
_logger.LogWarning("Blocked POST with invalid referer: {Referer} from IP {IP}", referer, ipAddress);
|
_logger.LogWarning("Blocked POST with invalid referer: {Referer} from IP {IP}", referer, ipAddress);
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,6 @@ namespace CatherineLynwood.Models
|
|||||||
{
|
{
|
||||||
#region Public Properties
|
#region Public Properties
|
||||||
|
|
||||||
[Required, StringLength(50)]
|
|
||||||
public string ApprovedSender { get; set; }
|
|
||||||
|
|
||||||
[Required, StringLength(50)]
|
[Required, StringLength(50)]
|
||||||
public string ContentFit { get; set; }
|
public string ContentFit { get; set; }
|
||||||
|
|
||||||
@ -20,6 +17,9 @@ namespace CatherineLynwood.Models
|
|||||||
[Required, StringLength(100)]
|
[Required, StringLength(100)]
|
||||||
public string FullName { get; set; }
|
public string FullName { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(50)]
|
||||||
|
public string HasKindleAccess { get; set; }
|
||||||
|
|
||||||
[Key]
|
[Key]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
|||||||
12
CatherineLynwood/Models/AbTestOptions.cs
Normal file
12
CatherineLynwood/Models/AbTestOptions.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public sealed class AbTestOptions
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
// Percentage of users who should see variant B; the rest see A
|
||||||
|
public int DiscoveryBPercent { get; set; } = 50;
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,21 @@
|
|||||||
{
|
{
|
||||||
public class AccessCode
|
public class AccessCode
|
||||||
{
|
{
|
||||||
public int PageNumber { get; set; }
|
#region Public Properties
|
||||||
public int WordIndex { get; set; }
|
|
||||||
|
public int AccessLevel { get; set; }
|
||||||
|
|
||||||
|
// 1 = basic, 2 = retail, 3 = deluxe
|
||||||
|
public int BookID { get; set; }
|
||||||
|
|
||||||
public string ExpectedWord { get; set; }
|
public string ExpectedWord { get; set; }
|
||||||
public int AccessLevel { get; set; } // 1 = basic, 2 = retail, 3 = deluxe
|
|
||||||
public int BookNumber { get; set; } // 1, 2, or 3
|
public int PageNumber { get; set; }
|
||||||
|
|
||||||
|
public int WordIndex { get; set; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
|
||||||
|
// 1, 2, or 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,8 +4,8 @@ namespace CatherineLynwood.Models
|
|||||||
{
|
{
|
||||||
public class ArcReaderApplicationModel
|
public class ArcReaderApplicationModel
|
||||||
{
|
{
|
||||||
[Required(ErrorMessage = "Please enter your full name.")]
|
[Required(ErrorMessage = "Please enter your name.")]
|
||||||
[Display(Name = "Your Full Name", Prompt = "e.g. Catherine Lynwood")]
|
[Display(Name = "Your Name", Prompt = "e.g. Catherine")]
|
||||||
[StringLength(100, ErrorMessage = "Name must be under 100 characters.")]
|
[StringLength(100, ErrorMessage = "Name must be under 100 characters.")]
|
||||||
public string FullName { get; set; }
|
public string FullName { get; set; }
|
||||||
|
|
||||||
@ -15,13 +15,9 @@ namespace CatherineLynwood.Models
|
|||||||
[DataType(DataType.EmailAddress)]
|
[DataType(DataType.EmailAddress)]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please enter your Kindle email address.")]
|
[Display(Name = "Your Kindle Email", Prompt = "e.g. yourname@kindle.com")]
|
||||||
[Display(Name = "Kindle Email Address", Prompt = "e.g. yourname")]
|
[EmailAddress(ErrorMessage = "Please enter a valid Kindle email address.")]
|
||||||
public string KindleEmail { get; set; }
|
public string? KindleEmail { get; set; } // Now optional
|
||||||
|
|
||||||
[Required(ErrorMessage = "Please select whether you've approved the sender.")]
|
|
||||||
[Display(Name = "Have you added my sender email to your Approved Senders list?")]
|
|
||||||
public string ApprovedSender { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Where do you plan to post your review?")]
|
[Display(Name = "Where do you plan to post your review?")]
|
||||||
public List<string> Platforms { get; set; }
|
public List<string> Platforms { get; set; }
|
||||||
@ -50,5 +46,10 @@ namespace CatherineLynwood.Models
|
|||||||
[DataType(DataType.MultilineText)]
|
[DataType(DataType.MultilineText)]
|
||||||
[StringLength(1000, ErrorMessage = "Please keep this under 1000 characters.")]
|
[StringLength(1000, ErrorMessage = "Please keep this under 1000 characters.")]
|
||||||
public string? ExtraNotes { get; set; }
|
public string? ExtraNotes { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please indicate whether you can receive the ARC via Kindle.")]
|
||||||
|
[Display(Name = "Do you have Kindle access?")]
|
||||||
|
public string HasKindleAccess { get; set; } // Expected values: "Yes" or "No"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
17
CatherineLynwood/Models/FlagSupportViewModel.cs
Normal file
17
CatherineLynwood/Models/FlagSupportViewModel.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class FlagSupportViewModel
|
||||||
|
{
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
CatherineLynwood/Models/GeoIpResult.cs
Normal file
27
CatherineLynwood/Models/GeoIpResult.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class GeoIpResult
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public string City { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string CountryName { get; set; } = "";
|
||||||
|
|
||||||
|
public string Ip { get; set; } = "";
|
||||||
|
|
||||||
|
public string Iso2 { get; set; } = "";
|
||||||
|
|
||||||
|
public DateTime LastSeenUtc { get; set; }
|
||||||
|
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string QueryString { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string UserAgent { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
13
CatherineLynwood/Models/HttpContextItemKeys.cs
Normal file
13
CatherineLynwood/Models/HttpContextItemKeys.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public static class HttpContextItemKeys
|
||||||
|
{
|
||||||
|
#region Public Fields
|
||||||
|
|
||||||
|
public const string CountryIso2 = "CountryIso2";
|
||||||
|
|
||||||
|
public const string CountryName = "CountryName";
|
||||||
|
|
||||||
|
#endregion Public Fields
|
||||||
|
}
|
||||||
|
}
|
||||||
13
CatherineLynwood/Models/LinkChoice.cs
Normal file
13
CatherineLynwood/Models/LinkChoice.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public sealed class LinkChoice
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
|
||||||
|
public string Url { get; set; } = "";
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
27
CatherineLynwood/Models/SoundTrackModel.cs
Normal file
27
CatherineLynwood/Models/SoundTrackModel.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
17
CatherineLynwood/Models/TitlePageViewModel.cs
Normal file
17
CatherineLynwood/Models/TitlePageViewModel.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public sealed class TitlePageViewModel
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public Reviews Reviews { get; set; } = new Reviews();
|
||||||
|
|
||||||
|
public string UserIso2 { get; set; } = "GB";
|
||||||
|
|
||||||
|
public string Src { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
using System.IO.Compression;
|
using CatherineLynwood.Middleware;
|
||||||
|
using CatherineLynwood.Models;
|
||||||
using CatherineLynwood.Middleware;
|
|
||||||
using CatherineLynwood.Services;
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.ResponseCompression;
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
|
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
using WebMarkupMin.AspNetCoreLatest;
|
using WebMarkupMin.AspNetCoreLatest;
|
||||||
|
|
||||||
namespace CatherineLynwood
|
namespace CatherineLynwood
|
||||||
@ -15,21 +17,44 @@ namespace CatherineLynwood
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Retrieve the connection string from appsettings.json
|
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||||
|
|
||||||
|
// DAL
|
||||||
builder.Services.AddSingleton(new DataAccess(connectionString));
|
builder.Services.AddSingleton(new DataAccess(connectionString));
|
||||||
|
|
||||||
|
// MVC
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
|
|
||||||
// Add IHttpContextAccessor for accessing HTTP context in tag helpers
|
// HttpContext accessor
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
builder.Services.AddHostedService<IndexNowBackgroundService>();
|
// Memory cache for IP cache
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
|
// HttpClient for general use
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
// ✅ Add session services (in-memory only)
|
// Named HttpClient for the GeoResolver with short timeout
|
||||||
|
builder.Services.AddHttpClient(nameof(GeoResolver), c => c.Timeout = TimeSpan.FromSeconds(3));
|
||||||
|
|
||||||
|
// Geo services
|
||||||
|
builder.Services.AddSingleton<IGeoResolver, GeoResolver>();
|
||||||
|
builder.Services.AddScoped<ICountryContext, CountryContext>();
|
||||||
|
|
||||||
|
// Background and other services you already use
|
||||||
|
builder.Services.AddHostedService<IndexNowBackgroundService>();
|
||||||
|
builder.Services.AddSingleton<RedirectsStore>(sp =>
|
||||||
|
{
|
||||||
|
var logger = sp.GetRequiredService<ILogger<RedirectsStore>>();
|
||||||
|
return new RedirectsStore("redirects.json", logger);
|
||||||
|
});
|
||||||
|
builder.Services.AddScoped<IAccessCodeService, AccessCodeService>();
|
||||||
|
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
||||||
|
builder.Services.AddSingleton<ChapterAudioMapCache>();
|
||||||
|
builder.Services.AddHostedService<ChapterAudioMapService>();
|
||||||
|
builder.Services.AddSingleton<AudioTokenService>();
|
||||||
|
|
||||||
|
// Session
|
||||||
builder.Services.AddSession(options =>
|
builder.Services.AddSession(options =>
|
||||||
{
|
{
|
||||||
options.IdleTimeout = TimeSpan.FromMinutes(30);
|
options.IdleTimeout = TimeSpan.FromMinutes(30);
|
||||||
@ -37,7 +62,7 @@ namespace CatherineLynwood
|
|||||||
options.Cookie.IsEssential = true;
|
options.Cookie.IsEssential = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ Add authentication with cookie settings
|
// Auth
|
||||||
builder.Services.AddAuthentication("MyCookieAuth")
|
builder.Services.AddAuthentication("MyCookieAuth")
|
||||||
.AddCookie("MyCookieAuth", options =>
|
.AddCookie("MyCookieAuth", options =>
|
||||||
{
|
{
|
||||||
@ -48,78 +73,63 @@ namespace CatherineLynwood
|
|||||||
options.ExpireTimeSpan = TimeSpan.FromHours(12);
|
options.ExpireTimeSpan = TimeSpan.FromHours(12);
|
||||||
options.SlidingExpiration = true;
|
options.SlidingExpiration = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// Add RedirectsStore as singleton
|
// Response compression
|
||||||
builder.Services.AddSingleton<RedirectsStore>(sp =>
|
|
||||||
{
|
|
||||||
var logger = sp.GetRequiredService<ILogger<RedirectsStore>>();
|
|
||||||
return new RedirectsStore("redirects.json", logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Register the book access code service
|
|
||||||
builder.Services.AddScoped<IAccessCodeService, AccessCodeService>();
|
|
||||||
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
|
||||||
|
|
||||||
builder.Services.AddSingleton<ChapterAudioMapCache>();
|
|
||||||
builder.Services.AddHostedService<ChapterAudioMapService>();
|
|
||||||
builder.Services.AddSingleton<AudioTokenService>();
|
|
||||||
|
|
||||||
// Add response compression services
|
|
||||||
builder.Services.AddResponseCompression(options =>
|
builder.Services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
options.EnableForHttps = true;
|
options.EnableForHttps = true;
|
||||||
options.Providers.Add<BrotliCompressionProvider>();
|
options.Providers.Add<BrotliCompressionProvider>();
|
||||||
options.Providers.Add<GzipCompressionProvider>();
|
options.Providers.Add<GzipCompressionProvider>();
|
||||||
});
|
});
|
||||||
|
builder.Services.Configure<BrotliCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
|
||||||
|
builder.Services.Configure<GzipCompressionProviderOptions>(o => o.Level = CompressionLevel.Fastest);
|
||||||
|
builder.Services.Configure<AbTestOptions>(builder.Configuration.GetSection("AbTest"));
|
||||||
|
|
||||||
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
|
|
||||||
{
|
|
||||||
options.Level = CompressionLevel.Fastest;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
|
// HTML minification
|
||||||
|
builder.Services.AddWebMarkupMin(o =>
|
||||||
{
|
{
|
||||||
options.Level = CompressionLevel.Fastest;
|
o.AllowMinificationInDevelopmentEnvironment = true;
|
||||||
});
|
|
||||||
|
|
||||||
// Add HTML minification
|
|
||||||
builder.Services.AddWebMarkupMin(options =>
|
|
||||||
{
|
|
||||||
options.AllowMinificationInDevelopmentEnvironment = true;
|
|
||||||
})
|
})
|
||||||
.AddHtmlMinification()
|
.AddHtmlMinification()
|
||||||
.AddHttpCompression()
|
.AddHttpCompression()
|
||||||
.AddXmlMinification()
|
.AddXmlMinification()
|
||||||
.AddXhtmlMinification();
|
.AddXhtmlMinification();
|
||||||
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
// Kestrel limits
|
||||||
{
|
builder.WebHost.ConfigureKestrel(o => { o.Limits.MaxRequestBodySize = 40 * 1024 * 1024; });
|
||||||
options.Limits.MaxRequestBodySize = 40 * 1024 * 1024; // 40MB
|
|
||||||
});
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Errors, HSTS
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Home/Error");
|
app.UseExceptionHandler("/Home/Error");
|
||||||
app.UseHsts(); // Adds the HSTS (HTTP Strict Transport Security) header
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If behind a proxy or CDN, capture real client IPs
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
|
||||||
|
ForwardLimit = 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Your existing custom middleware
|
||||||
app.UseMiddleware<SpamAndSecurityMiddleware>();
|
app.UseMiddleware<SpamAndSecurityMiddleware>();
|
||||||
app.UseMiddleware<HoneypotLoggingMiddleware>();
|
app.UseMiddleware<HoneypotLoggingMiddleware>();
|
||||||
app.UseMiddleware<RedirectMiddleware>();
|
app.UseMiddleware<RedirectMiddleware>();
|
||||||
|
app.UseMiddleware<EnsureSidMiddleware>();
|
||||||
|
app.UseMiddleware<GeoResolutionMiddleware>();
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseWebMarkupMin();
|
app.UseWebMarkupMin();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseSession();
|
|
||||||
|
|
||||||
// ✅ Authentication must come before Authorization
|
app.UseSession();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
@ -128,6 +138,7 @@ namespace CatherineLynwood
|
|||||||
pattern: "{controller=Home}/{action=Index}/{id?}");
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,37 @@
|
|||||||
using CatherineLynwood.Models;
|
using CatherineLynwood.Models;
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CatherineLynwood.Services
|
namespace CatherineLynwood.Services
|
||||||
{
|
{
|
||||||
public interface IAccessCodeService
|
public interface IAccessCodeService
|
||||||
{
|
{
|
||||||
Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync();
|
#region Public Methods
|
||||||
Task<(int accessLevel, int highestBook)> ValidateWordAsync(string userWord);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync();
|
||||||
|
|
||||||
|
Task<(int accessLevel, int highestBook)> ValidateWordAsync(string userWord);
|
||||||
|
|
||||||
|
#endregion Public Methods
|
||||||
|
}
|
||||||
|
|
||||||
public class AccessCodeService : IAccessCodeService
|
public class AccessCodeService : IAccessCodeService
|
||||||
{
|
{
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
private readonly List<AccessCode> _accessCodes;
|
private readonly List<AccessCode> _accessCodes;
|
||||||
|
|
||||||
|
#endregion Private Fields
|
||||||
|
|
||||||
|
#region Public Constructors
|
||||||
|
|
||||||
public AccessCodeService(DataAccess dataAccess)
|
public AccessCodeService(DataAccess dataAccess)
|
||||||
{
|
{
|
||||||
_accessCodes = dataAccess.GetAccessCodes();
|
_accessCodes = dataAccess.GetAccessCodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion Public Constructors
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
public Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync()
|
public Task<(int pageNumber, int wordIndex)> GetCurrentChallengeAsync()
|
||||||
{
|
{
|
||||||
var challenge = _accessCodes.First(); // current prompt
|
var challenge = _accessCodes.First(); // current prompt
|
||||||
@ -34,10 +44,11 @@ namespace CatherineLynwood.Services
|
|||||||
c.ExpectedWord.Equals(userWord, StringComparison.OrdinalIgnoreCase));
|
c.ExpectedWord.Equals(userWord, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (match != null)
|
if (match != null)
|
||||||
return Task.FromResult((match.AccessLevel, match.BookNumber));
|
return Task.FromResult((match.AccessLevel, match.BookID));
|
||||||
|
|
||||||
return Task.FromResult((0, 0));
|
return Task.FromResult((0, 0));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
#endregion Public Methods
|
||||||
|
}
|
||||||
}
|
}
|
||||||
20
CatherineLynwood/Services/CountryContext.cs
Normal file
20
CatherineLynwood/Services/CountryContext.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Services
|
||||||
|
{
|
||||||
|
public sealed class CountryContext : ICountryContext
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _http;
|
||||||
|
|
||||||
|
public CountryContext(IHttpContextAccessor http)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Iso2 =>
|
||||||
|
_http.HttpContext?.Items[HttpContextItemKeys.CountryIso2] as string;
|
||||||
|
|
||||||
|
public string? CountryName =>
|
||||||
|
_http.HttpContext?.Items[HttpContextItemKeys.CountryName] as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
using SixLabors.ImageSharp.Web.Commands.Converters;
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
namespace CatherineLynwood.Services
|
namespace CatherineLynwood.Services
|
||||||
@ -227,7 +222,7 @@ namespace CatherineLynwood.Services
|
|||||||
WordIndex = GetDataInt(rdr, "WordIndex"),
|
WordIndex = GetDataInt(rdr, "WordIndex"),
|
||||||
ExpectedWord = GetDataString(rdr, "ExpectedWord"),
|
ExpectedWord = GetDataString(rdr, "ExpectedWord"),
|
||||||
AccessLevel = GetDataInt(rdr, "AccessLevel"),
|
AccessLevel = GetDataInt(rdr, "AccessLevel"),
|
||||||
BookNumber = GetDataInt(rdr, "BookNumber")
|
BookID = GetDataInt(rdr, "BookID")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,6 +236,52 @@ namespace CatherineLynwood.Services
|
|||||||
return accessCodes;
|
return accessCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ARCReaderList> GetAllARCReadersAsync()
|
||||||
|
{
|
||||||
|
ARCReaderList arcReaderList = new ARCReaderList();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetAllARCReaders";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
arcReaderList.Applications.Add(new ARCReaderApplication
|
||||||
|
{
|
||||||
|
HasKindleAccess = GetDataString(rdr, "HasKindleAccess"),
|
||||||
|
ContentFit = GetDataString(rdr, "ContentFit"),
|
||||||
|
Email = GetDataString(rdr, "Email"),
|
||||||
|
ExtraNotes = GetDataString(rdr, "ExtraNotes"),
|
||||||
|
FullName = GetDataString(rdr, "FullName"),
|
||||||
|
KindleEmail = GetDataString(rdr, "KindleEmail"),
|
||||||
|
Platforms = GetDataString(rdr, "Platforms"),
|
||||||
|
PlatformsOther = GetDataString(rdr, "PlatformsOther"),
|
||||||
|
PreviewChapters = GetDataString(rdr, "PreviewChapters"),
|
||||||
|
ReviewCommitment = GetDataString(rdr, "ReviewCommitment"),
|
||||||
|
ReviewLink = GetDataString(rdr, "ReviewLink"),
|
||||||
|
SubmittedAt = GetDataDate(rdr, "SubmittedAt"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arcReaderList;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<BlogSummaryResponse>> GetAllBlogsAsync()
|
public async Task<List<BlogSummaryResponse>> GetAllBlogsAsync()
|
||||||
{
|
{
|
||||||
List<BlogSummaryResponse> list = new List<BlogSummaryResponse>();
|
List<BlogSummaryResponse> list = new List<BlogSummaryResponse>();
|
||||||
@ -282,52 +323,6 @@ namespace CatherineLynwood.Services
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ARCReaderList> GetAllARCReadersAsync()
|
|
||||||
{
|
|
||||||
ARCReaderList arcReaderList = new ARCReaderList();
|
|
||||||
|
|
||||||
using (SqlConnection conn = new SqlConnection(_connectionString))
|
|
||||||
{
|
|
||||||
using (SqlCommand cmd = new SqlCommand())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await conn.OpenAsync();
|
|
||||||
cmd.Connection = conn;
|
|
||||||
cmd.CommandType = CommandType.StoredProcedure;
|
|
||||||
cmd.CommandText = "GetAllARCReaders";
|
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
|
||||||
{
|
|
||||||
while (await rdr.ReadAsync())
|
|
||||||
{
|
|
||||||
arcReaderList.Applications.Add(new ARCReaderApplication
|
|
||||||
{
|
|
||||||
ApprovedSender = GetDataString(rdr, "ApprovedSender"),
|
|
||||||
ContentFit = GetDataString(rdr, "ContentFit"),
|
|
||||||
Email = GetDataString(rdr, "Email"),
|
|
||||||
ExtraNotes = GetDataString(rdr, "ExtraNotes"),
|
|
||||||
FullName = GetDataString(rdr, "FullName"),
|
|
||||||
KindleEmail = GetDataString(rdr, "KindleEmail"),
|
|
||||||
Platforms = GetDataString(rdr, "Platforms"),
|
|
||||||
PlatformsOther = GetDataString(rdr, "PlatformsOther"),
|
|
||||||
PreviewChapters = GetDataString(rdr, "PreviewChapters"),
|
|
||||||
ReviewCommitment = GetDataString(rdr, "ReviewCommitment"),
|
|
||||||
ReviewLink = GetDataString(rdr, "ReviewLink"),
|
|
||||||
SubmittedAt = GetDataDate(rdr, "SubmittedAt"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return arcReaderList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<BlogAdminIndex> GetBlogAdminIndexAsync()
|
public async Task<BlogAdminIndex> GetBlogAdminIndexAsync()
|
||||||
{
|
{
|
||||||
BlogAdminIndex blogAdminIndex = new BlogAdminIndex();
|
BlogAdminIndex blogAdminIndex = new BlogAdminIndex();
|
||||||
@ -548,6 +543,77 @@ namespace CatherineLynwood.Services
|
|||||||
return blogIndex;
|
return blogIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<List<BlogPost>> GetDueBlogPostsAsync()
|
public async Task<List<BlogPost>> GetDueBlogPostsAsync()
|
||||||
{
|
{
|
||||||
List<BlogPost> blogPosts = new List<BlogPost>();
|
List<BlogPost> blogPosts = new List<BlogPost>();
|
||||||
@ -584,6 +650,50 @@ namespace CatherineLynwood.Services
|
|||||||
return blogPosts;
|
return blogPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<GeoIpResult?> GetGeoIpAsync(string ip)
|
||||||
|
{
|
||||||
|
GeoIpResult? result = null;
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetGeoIp"; // your stored procedure name
|
||||||
|
cmd.Parameters.AddWithValue("@Ip", ip);
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
if (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
result = new GeoIpResult
|
||||||
|
{
|
||||||
|
Ip = GetDataString(rdr, "IP"),
|
||||||
|
Iso2 = GetDataString(rdr, "ISO2"),
|
||||||
|
CountryName = GetDataString(rdr, "CountryName"),
|
||||||
|
LastSeenUtc = GetDataDate(rdr, "LastSeenUtc"),
|
||||||
|
Source = GetDataString(rdr, "Source"),
|
||||||
|
UserAgent = GetDataString(rdr, "UserAgent"),
|
||||||
|
Path = GetDataString(rdr, "Path"),
|
||||||
|
City = GetDataString(rdr, "City")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// log if you want
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Questions> GetQuestionsAsync()
|
public async Task<Questions> GetQuestionsAsync()
|
||||||
{
|
{
|
||||||
Questions questions = new Questions();
|
Questions questions = new Questions();
|
||||||
@ -660,7 +770,7 @@ namespace CatherineLynwood.Services
|
|||||||
return selectListItems;
|
return selectListItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Reviews> GetReviewsAsync()
|
public async Task<Reviews> GetReviewsAsync(string title)
|
||||||
{
|
{
|
||||||
Reviews reviews = new Reviews();
|
Reviews reviews = new Reviews();
|
||||||
|
|
||||||
@ -674,6 +784,7 @@ namespace CatherineLynwood.Services
|
|||||||
cmd.Connection = conn;
|
cmd.Connection = conn;
|
||||||
cmd.CommandType = CommandType.StoredProcedure;
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
cmd.CommandText = "GetReviews";
|
cmd.CommandText = "GetReviews";
|
||||||
|
cmd.Parameters.AddWithValue("@Title", title);
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
{
|
{
|
||||||
@ -700,6 +811,85 @@ namespace CatherineLynwood.Services
|
|||||||
return reviews;
|
return reviews;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetTrailerLikes";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
string country = GetDataString(rdr, "Country");
|
||||||
|
int likes = GetDataInt(rdr, "Likes");
|
||||||
|
|
||||||
|
flagSupportViewModel.FlagCounts.Add(new FlagCount
|
||||||
|
{
|
||||||
|
Key = country,
|
||||||
|
Value = likes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flagSupportViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> MarkAsNotifiedAsync(int blogID)
|
public async Task<bool> MarkAsNotifiedAsync(int blogID)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
@ -727,6 +917,47 @@ namespace CatherineLynwood.Services
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SaveARCReaderApplication(ArcReaderApplicationModel arcReaderApplication)
|
||||||
|
{
|
||||||
|
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 = "SaveARCReader";
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("@FullName", arcReaderApplication.FullName);
|
||||||
|
cmd.Parameters.AddWithValue("@Email", arcReaderApplication.Email);
|
||||||
|
cmd.Parameters.AddWithValue("@KindleEmail", string.IsNullOrWhiteSpace(arcReaderApplication.KindleEmail) ? (object)DBNull.Value : arcReaderApplication.KindleEmail);
|
||||||
|
cmd.Parameters.AddWithValue("@HasKindleAccess", arcReaderApplication.HasKindleAccess);
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("@Platforms", arcReaderApplication.Platforms != null ? string.Join(",", arcReaderApplication.Platforms) : (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@PreviewChapters", arcReaderApplication.PreviewChapters != null ? string.Join(",", arcReaderApplication.PreviewChapters) : (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@PlatformsOther", string.IsNullOrWhiteSpace(arcReaderApplication.PlatformsOther) ? (object)DBNull.Value : arcReaderApplication.PlatformsOther);
|
||||||
|
cmd.Parameters.AddWithValue("@ReviewLink", string.IsNullOrWhiteSpace(arcReaderApplication.ReviewLink) ? (object)DBNull.Value : arcReaderApplication.ReviewLink);
|
||||||
|
cmd.Parameters.AddWithValue("@ContentFit", arcReaderApplication.ContentFit ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@ReviewCommitment", arcReaderApplication.ReviewCommitment ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@ExtraNotes", string.IsNullOrWhiteSpace(arcReaderApplication.ExtraNotes) ? (object)DBNull.Value : arcReaderApplication.ExtraNotes);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Optionally log or rethrow
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> SaveBlogToDatabase(BlogPostRequest blog)
|
public async Task<bool> SaveBlogToDatabase(BlogPostRequest blog)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
@ -765,7 +996,56 @@ namespace CatherineLynwood.Services
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SaveContact(Contact contact)
|
public async Task<bool> SaveBuyClick(
|
||||||
|
DateTime dateTimeUtc,
|
||||||
|
string slug,
|
||||||
|
string countryGroup,
|
||||||
|
string ip,
|
||||||
|
string country,
|
||||||
|
string userAgent,
|
||||||
|
string referer,
|
||||||
|
string sessionId,
|
||||||
|
string src
|
||||||
|
)
|
||||||
|
{
|
||||||
|
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("@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("@SessionId", (object?)sessionId ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@Src", (object?)src ?? DBNull.Value);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
// optional: log the exception somewhere central
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SaveContact(Contact contact, bool subscribe = false)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
|
||||||
@ -781,10 +1061,12 @@ namespace CatherineLynwood.Services
|
|||||||
cmd.CommandText = "SaveContact";
|
cmd.CommandText = "SaveContact";
|
||||||
cmd.Parameters.AddWithValue("@Name", contact.Name);
|
cmd.Parameters.AddWithValue("@Name", contact.Name);
|
||||||
cmd.Parameters.AddWithValue("@EmailAddress", contact.EmailAddress);
|
cmd.Parameters.AddWithValue("@EmailAddress", contact.EmailAddress);
|
||||||
|
cmd.Parameters.AddWithValue("@Subscribe", subscribe);
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -792,7 +1074,40 @@ namespace CatherineLynwood.Services
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SaveARCReaderApplication(ArcReaderApplicationModel arcReaderApplication)
|
public async Task<int> SaveFlagClick(string country)
|
||||||
|
{
|
||||||
|
int likes = 0;
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "SaveTrailerLike";
|
||||||
|
cmd.Parameters.AddWithValue("@Country", country);
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
likes = GetDataInt(rdr, "Likes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SaveGeoIpAsync(GeoIpResult geo)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
|
||||||
@ -805,24 +1120,22 @@ namespace CatherineLynwood.Services
|
|||||||
await conn.OpenAsync();
|
await conn.OpenAsync();
|
||||||
cmd.Connection = conn;
|
cmd.Connection = conn;
|
||||||
cmd.CommandType = CommandType.StoredProcedure;
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
cmd.CommandText = "SaveARCReader";
|
cmd.CommandText = "SaveGeoIp"; // your stored procedure name
|
||||||
cmd.Parameters.AddWithValue("@FullName", arcReaderApplication.FullName);
|
|
||||||
cmd.Parameters.AddWithValue("@Email", arcReaderApplication.Email);
|
cmd.Parameters.AddWithValue("@Ip", geo.Ip);
|
||||||
cmd.Parameters.AddWithValue("@KindleEmail", arcReaderApplication.KindleEmail);
|
cmd.Parameters.AddWithValue("@Iso2", geo.Iso2);
|
||||||
cmd.Parameters.AddWithValue("@ApprovedSender", arcReaderApplication.ApprovedSender ?? (object)DBNull.Value);
|
cmd.Parameters.AddWithValue("@CountryName", geo.CountryName);
|
||||||
cmd.Parameters.AddWithValue("@Platforms", arcReaderApplication.Platforms != null ? string.Join(",", arcReaderApplication.Platforms) : (object)DBNull.Value);
|
cmd.Parameters.AddWithValue("@LastSeenUtc", geo.LastSeenUtc);
|
||||||
cmd.Parameters.AddWithValue("@PreviewChapters", arcReaderApplication.PreviewChapters != null ? string.Join(",", arcReaderApplication.PreviewChapters) : (object)DBNull.Value);
|
cmd.Parameters.AddWithValue("@Source", geo.Source ?? (object)DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@PlatformsOther", string.IsNullOrWhiteSpace(arcReaderApplication.PlatformsOther) ? (object)DBNull.Value : arcReaderApplication.PlatformsOther);
|
cmd.Parameters.AddWithValue("@UserAgent", geo.UserAgent ?? (object)DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@ReviewLink", string.IsNullOrWhiteSpace(arcReaderApplication.ReviewLink) ? (object)DBNull.Value : arcReaderApplication.ReviewLink);
|
cmd.Parameters.AddWithValue("@Path", geo.Path ?? (object)DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@ContentFit", arcReaderApplication.ContentFit ?? (object)DBNull.Value);
|
cmd.Parameters.AddWithValue("@QueryString", geo.QueryString ?? (object)DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@ReviewCommitment", arcReaderApplication.ReviewCommitment ?? (object)DBNull.Value);
|
cmd.Parameters.AddWithValue("@City", geo.City ?? (object)DBNull.Value);
|
||||||
cmd.Parameters.AddWithValue("@ExtraNotes", string.IsNullOrWhiteSpace(arcReaderApplication.ExtraNotes) ? (object)DBNull.Value : arcReaderApplication.ExtraNotes);
|
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Optionally log or rethrow
|
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -831,7 +1144,6 @@ namespace CatherineLynwood.Services
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<bool> UpdateBlogAsync(Blog blog)
|
public async Task<bool> UpdateBlogAsync(Blog blog)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
|||||||
82
CatherineLynwood/Services/GeoResolver.cs
Normal file
82
CatherineLynwood/Services/GeoResolver.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Services
|
||||||
|
{
|
||||||
|
public sealed class GeoResolver : IGeoResolver
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly DataAccess _dataAccess;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly ILogger<GeoResolver> _logger;
|
||||||
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(6);
|
||||||
|
|
||||||
|
public GeoResolver(IMemoryCache cache, DataAccess dataAccess, IHttpClientFactory httpClientFactory, ILogger<GeoResolver> logger)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GeoIpResult?> ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var ipStr = ip.ToString();
|
||||||
|
|
||||||
|
if (_cache.TryGetValue<GeoIpResult>(ipStr, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
// 1) Database lookup
|
||||||
|
var db = await _dataAccess.GetGeoIpAsync(ipStr);
|
||||||
|
if (db is not null)
|
||||||
|
{
|
||||||
|
_cache.Set(ipStr, db, CacheTtl);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) External API fallback
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient(nameof(GeoResolver));
|
||||||
|
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() ?? "UK";
|
||||||
|
var country = root.GetProperty("country").GetString();
|
||||||
|
var city = root.GetProperty("city").GetString() ?? "";
|
||||||
|
|
||||||
|
var geo = new GeoIpResult
|
||||||
|
{
|
||||||
|
Ip = ipStr,
|
||||||
|
Iso2 = iso2,
|
||||||
|
CountryName = country ?? "",
|
||||||
|
LastSeenUtc = DateTime.UtcNow,
|
||||||
|
Source = "ip-api",
|
||||||
|
UserAgent = userAgent,
|
||||||
|
Path = path,
|
||||||
|
QueryString = queryString,
|
||||||
|
City = city
|
||||||
|
};
|
||||||
|
|
||||||
|
await _dataAccess.SaveGeoIpAsync(geo);
|
||||||
|
_cache.Set(ipStr, geo, CacheTtl);
|
||||||
|
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Geo lookup failed for {IP}", ipStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
CatherineLynwood/Services/ICountryContext.cs
Normal file
13
CatherineLynwood/Services/ICountryContext.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace CatherineLynwood.Services
|
||||||
|
{
|
||||||
|
public interface ICountryContext
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
string? CountryName { get; }
|
||||||
|
|
||||||
|
string? Iso2 { get; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
11
CatherineLynwood/Services/IGeoResolver.cs
Normal file
11
CatherineLynwood/Services/IGeoResolver.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Services
|
||||||
|
{
|
||||||
|
public interface IGeoResolver
|
||||||
|
{
|
||||||
|
Task<GeoIpResult?> ResolveAsync(IPAddress ip, string path, string queryString, string userAgent, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ namespace CatherineLynwood.Services
|
|||||||
{
|
{
|
||||||
public interface IEmailService
|
public interface IEmailService
|
||||||
{
|
{
|
||||||
Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact);
|
Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact, bool sendToUser = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SmtpEmailService : IEmailService
|
public class SmtpEmailService : IEmailService
|
||||||
@ -24,12 +24,22 @@ namespace CatherineLynwood.Services
|
|||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact)
|
public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact, bool sendToUser = false)
|
||||||
{
|
{
|
||||||
var message = new MimeMessage();
|
var message = new MimeMessage();
|
||||||
message.From.Add(new MailboxAddress("Catherine Lynwood", _config["Smtp:Sender"]));
|
message.From.Add(new MailboxAddress("Catherine Lynwood", _config["Smtp:Sender"]));
|
||||||
message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com"));
|
|
||||||
message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress));
|
if (sendToUser)
|
||||||
|
{
|
||||||
|
message.To.Add(new MailboxAddress(contact.Name, contact.EmailAddress));
|
||||||
|
message.Bcc.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com"));
|
||||||
|
message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
message.Subject = subject;
|
message.Subject = subject;
|
||||||
|
|
||||||
|
|||||||
@ -8,27 +8,42 @@ namespace CatherineLynwood.TagHelpers
|
|||||||
#region Public Properties
|
#region Public Properties
|
||||||
|
|
||||||
public DateTime? ArticleModifiedTime { get; set; }
|
public DateTime? ArticleModifiedTime { get; set; }
|
||||||
|
|
||||||
public DateTime? ArticlePublishedTime { get; set; }
|
public DateTime? ArticlePublishedTime { get; set; }
|
||||||
|
|
||||||
public string MetaAuthor { get; set; }
|
public string MetaAuthor { get; set; }
|
||||||
|
|
||||||
public string MetaDescription { get; set; }
|
public string MetaDescription { get; set; }
|
||||||
|
|
||||||
public string MetaImage { get; set; }
|
public string MetaImage { get; set; }
|
||||||
|
|
||||||
public string MetaImageAlt { get; set; }
|
public string MetaImageAlt { get; set; }
|
||||||
|
|
||||||
|
public string MetaImagePNG { get; set; }
|
||||||
|
|
||||||
public string MetaKeywords { get; set; }
|
public string MetaKeywords { get; set; }
|
||||||
|
|
||||||
public string MetaTitle { get; set; }
|
public string MetaTitle { get; set; }
|
||||||
|
|
||||||
public string MetaUrl { get; set; }
|
public string MetaUrl { get; set; }
|
||||||
|
|
||||||
public string OgSiteName { get; set; }
|
public string OgSiteName { get; set; }
|
||||||
|
|
||||||
public string OgType { get; set; } = "article";
|
public string OgType { get; set; } = "article";
|
||||||
|
|
||||||
public string TwitterCardType { get; set; } = "summary_large_image";
|
public string TwitterCardType { get; set; } = "summary_large_image";
|
||||||
|
|
||||||
public string TwitterCreatorHandle { get; set; }
|
public string TwitterCreatorHandle { get; set; }
|
||||||
|
|
||||||
public int? TwitterPlayerHeight { get; set; }
|
public int? TwitterPlayerHeight { get; set; }
|
||||||
|
|
||||||
public int? TwitterPlayerWidth { get; set; }
|
public int? TwitterPlayerWidth { get; set; }
|
||||||
|
|
||||||
public string TwitterSiteHandle { get; set; }
|
public string TwitterSiteHandle { get; set; }
|
||||||
|
|
||||||
public string TwitterVideoUrl { get; set; }
|
public string TwitterVideoUrl { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion Public Properties
|
||||||
|
|
||||||
#region Public Methods
|
#region Public Methods
|
||||||
|
|
||||||
@ -63,6 +78,9 @@ namespace CatherineLynwood.TagHelpers
|
|||||||
if (!string.IsNullOrWhiteSpace(MetaImage))
|
if (!string.IsNullOrWhiteSpace(MetaImage))
|
||||||
metaTags.AppendLine($"<meta property=\"og:image\" content=\"{MetaImage}\">");
|
metaTags.AppendLine($"<meta property=\"og:image\" content=\"{MetaImage}\">");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(MetaImagePNG))
|
||||||
|
metaTags.AppendLine($"<meta property=\"og:image\" content=\"{MetaImagePNG}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaImageAlt))
|
if (!string.IsNullOrWhiteSpace(MetaImageAlt))
|
||||||
metaTags.AppendLine($"<meta property=\"og:image:alt\" content=\"{MetaImageAlt}\">");
|
metaTags.AppendLine($"<meta property=\"og:image:alt\" content=\"{MetaImageAlt}\">");
|
||||||
|
|
||||||
@ -124,6 +142,6 @@ namespace CatherineLynwood.TagHelpers
|
|||||||
output.Content.SetHtmlContent(metaTags.ToString());
|
output.Content.SetHtmlContent(metaTags.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion Public Methods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,9 +2,11 @@
|
|||||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||||
|
|
||||||
using SixLabors.Fonts;
|
using SixLabors.Fonts;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
using SixLabors.ImageSharp.Formats.Webp;
|
using SixLabors.ImageSharp.Formats.Webp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace CatherineLynwood.TagHelpers
|
namespace CatherineLynwood.TagHelpers
|
||||||
{
|
{
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<p class="card-text mb-1"><strong>Email:</strong> @item.Email</p>
|
<p class="card-text mb-1"><strong>Email:</strong> @item.Email</p>
|
||||||
<p class="card-text mb-1"><strong>Kindle Email:</strong> @item.KindleEmail</p>
|
<p class="card-text mb-1"><strong>Kindle Email:</strong> @item.KindleEmail</p>
|
||||||
<p class="card-text mb-1"><strong>Approved Sender:</strong> @item.ApprovedSender</p>
|
<p class="card-text mb-1"><strong>Has Kindle Access:</strong> @item.HasKindleAccess</p>
|
||||||
<p class="card-text mb-1"><strong>Platforms:</strong> @item.Platforms</p>
|
<p class="card-text mb-1"><strong>Platforms:</strong> @item.Platforms</p>
|
||||||
@if (!string.IsNullOrWhiteSpace(item.PlatformsOther))
|
@if (!string.IsNullOrWhiteSpace(item.PlatformsOther))
|
||||||
{
|
{
|
||||||
|
|||||||
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-audiobook.png"
|
||||||
|
class="img-fluid rounded"
|
||||||
|
alt="The Alpha Flame Audiobook 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";
|
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 1";
|
||||||
}
|
}
|
||||||
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="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="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="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="Index">Discovery</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Chapter 1 - Drowning in Silence</li>
|
<li class="breadcrumb-item active" aria-current="page">Chapter 1 - Drowning in Silence</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Audio and Text -->
|
<!-- Header -->
|
||||||
<div class="col-lg-7">
|
<div class="text-center mb-5">
|
||||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
<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">
|
<!-- Excerpt Content -->
|
||||||
<video controls="controls" poster="/images/Chapter-1.png" class="rounded-5">
|
<div class="row gx-5">
|
||||||
<source src="~/videos/Chapter-1-preview.mp4" type="video/mp4"/>
|
<!-- Scene Image -->
|
||||||
</video>
|
<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>
|
<!-- Audio and Text -->
|
||||||
<p class="text-center text-muted small pt-2">
|
<div class="col-lg-7">
|
||||||
Watch Beth narrating part of Chapter 1 - Drowning in Silence.
|
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
||||||
</p>
|
|
||||||
<!-- Audio Player -->
|
<div class="ratio ratio-16x9">
|
||||||
<div class="audio-player text-center">
|
<video controls="controls" poster="/images/Chapter-1.png" class="rounded-5">
|
||||||
<audio controls>
|
<source src="~/videos/Chapter-1-preview.mp4" type="video/mp4" />
|
||||||
<source src="/audio/the-alpha-flame-discovery-chapter-1.mp3" type="audio/mpeg">
|
</video>
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>
|
</div>
|
||||||
<p class="text-center text-muted small">
|
<p class="text-center text-muted small pt-2">
|
||||||
Listen to Beth narrating the complete Chapter 1 - Drowning in Silence.
|
Watch Beth narrating part of 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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,92 +2,94 @@
|
|||||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13";
|
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 13";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="container">
|
||||||
<div class="col-12">
|
<div class="row">
|
||||||
<nav aria-label="breadcrumb">
|
<div class="col-12">
|
||||||
<ol class="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</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">Chapter 13 - A Name She Never Owned</li>
|
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||||
</ol>
|
<li class="breadcrumb-item active" aria-current="page">Chapter 13 - A Name She Never Owned</li>
|
||||||
</nav>
|
</ol>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Audio and Text -->
|
<!-- Header -->
|
||||||
<div class="col-lg-7">
|
<div class="text-center mb-5">
|
||||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
<h1 class="fw-bold">Chapter 13 - A Name She Never Owned - Susie</h1>
|
||||||
<!-- Audio Player -->
|
<p>An exclusive glimpse into Susie's story</p>
|
||||||
<div class="audio-player text-center">
|
</div>
|
||||||
<audio controls>
|
|
||||||
<source src="/audio/the-alpha-flame-discovery-chapter-13.mp3" type="audio/mpeg">
|
<!-- Excerpt Content -->
|
||||||
Your browser does not support the audio element.
|
<div class="row gx-5">
|
||||||
</audio>
|
<!-- Scene Image -->
|
||||||
<p class="text-center text-muted small">
|
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||||
Listen to Susie narrating a large excerpt from Chapter 13 - A Name She Never Owned
|
<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>
|
||||||
</p>
|
|
||||||
</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>
|
||||||
|
|
||||||
|
|
||||||
<!-- Text Content -->
|
<!-- Text Content -->
|
||||||
<div class="chapter-text">
|
<div class="chapter-text">
|
||||||
<p class="chapter-title">A Name She Never Owned - Susie</p>
|
<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>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>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>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><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>“Hi,” he said, smiling nervously. “I wondered if you’d remember.”</p>
|
||||||
<p>“Of course,” I replied, returning his smile.</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><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>“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>“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><em>Ben’s smile deepened. “Are you ready?”</em></p>
|
||||||
<p>“Sure.”</p>
|
<p>“Sure.”</p>
|
||||||
<p><em>He started the car and pulled away, driving slowly, a little nervously.</em></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>“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>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><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>“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>“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>“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><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>“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>“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>“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>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><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>“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>“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><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>“Yeah. What’s wrong with that?”</p>
|
||||||
<p>“Did you tell him I’m a prostitute?”</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><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>“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>“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>“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>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>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>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><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>“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>“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>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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,75 +2,77 @@
|
|||||||
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2";
|
ViewData["Title"] = "The Alpha Flame: Discovery Chapter 2";
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="container">
|
||||||
<div class="col-12">
|
<div class="row">
|
||||||
<nav aria-label="breadcrumb">
|
<div class="col-12">
|
||||||
<ol class="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Index">The Alpha Flame</a></li>
|
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</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">Chapter 2 - The Last Lesson</li>
|
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Index">Discovery</a></li>
|
||||||
</ol>
|
<li class="breadcrumb-item active" aria-current="page">Chapter 2 - The Last Lesson</li>
|
||||||
</nav>
|
</ol>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Audio and Text -->
|
<!-- Header -->
|
||||||
<div class="col-lg-7">
|
<div class="text-center mb-5">
|
||||||
<div class="bg-white rounded-5 border border-3 border-dark shadow-lg p-3">
|
<h1 class="fw-bold">Chapter 2- The Last Lesson - Maggie</h1>
|
||||||
<!-- Audio Player -->
|
<p>An exclusive glimpse into Maggie's story</p>
|
||||||
<div class="audio-player text-center">
|
</div>
|
||||||
<audio controls>
|
|
||||||
<source src="/audio/the-alpha-flame-discovery-chapter-2.mp3" type="audio/mpeg">
|
<!-- Excerpt Content -->
|
||||||
Your browser does not support the audio element.
|
<div class="row gx-5">
|
||||||
</audio>
|
<!-- Scene Image -->
|
||||||
<p class="text-center text-muted small">
|
<div class="col-lg-5 mb-4 mb-lg-0">
|
||||||
Listen to Maggie narrating the complete Chapter 2 - The Last Lesson,
|
<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>
|
||||||
</p>
|
|
||||||
</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>
|
||||||
|
|
||||||
|
|
||||||
<!-- Text Content -->
|
<!-- Text Content -->
|
||||||
<div class="chapter-text">
|
<div class="chapter-text">
|
||||||
<p class="chapter-title">The Last Lesson - Maggie</p>
|
<p class="chapter-title">The Last Lesson - Maggie</p>
|
||||||
<p><em>There was a knock at the door.</em></p>
|
<p><em>There was a knock at the door.</em></p>
|
||||||
<p>“Colin’s here,” called mum.</p>
|
<p>“Colin’s here,” called mum.</p>
|
||||||
<p>“Okay, I’ll be there in a second,” I replied.</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 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><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>“Maggie,” called Mum, “you’re going to be late.”</p>
|
||||||
<p>“Coming.”</p>
|
<p>“Coming.”</p>
|
||||||
<p><em>I walked into the hallway where mum was talking to Colin.</em></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>“How do I look?” I asked.</p>
|
||||||
<p>Mum looked at me. “No bra?” she queried.</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><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>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>“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><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>“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>“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><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>“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><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>“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>“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><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>
|
<p>Calmly, he said, “Don’t worry. Let’s just try that again...”</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
<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="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="Index">Discovery</a></li>
|
||||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Epilogue</li>
|
<li class="breadcrumb-item active" aria-current="page">Epilogue</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Extras";
|
ViewData["Title"] = "The Alpha Flame: Discovery Extras";
|
||||||
|
|
||||||
int? accessLevel = Context.Session.GetInt32("BookAccessLevel");
|
int? accessLevel = Context.Session.GetInt32("BookAccessLevel");
|
||||||
int? accessBook = Context.Session.GetInt32("BookAccessMax");
|
int? accessBook = Context.Session.GetInt32("BookAccessMax");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -19,92 +19,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<h1 class="extras-header">Your Exclusive Extras</h1>
|
<h1 class="extras-header">The Alpha Flame Discovery Exclusive Extras</h1>
|
||||||
|
<div class="extras-grid mt-4">
|
||||||
@if (accessBook == 1)
|
@if (accessLevel >= 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>
|
|
||||||
}
|
|
||||||
@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 extra-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Epilogue</h5>
|
<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>
|
<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>
|
<a asp-action="Epilogue" class="btn btn-dark btn-sm">Read or Listen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
@if (accessLevel >= 2)
|
||||||
|
{
|
||||||
<div class="card extra-card">
|
<div class="card extra-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Discovery Scrap Book</h5>
|
<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>
|
<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>
|
<a asp-action="ScrapBook" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card extra-card">
|
<div class="card extra-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Rubery Hill Photo Archive</h5>
|
<h5 class="card-title">Discovery Soundtrack</h5>
|
||||||
<p class="card-text">Explore historical photos and floor plans of the real Rubery Hill Hospital, the eerie inspiration behind key scenes.</p>
|
<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 href="/extras/rubery-hill-photos" class="btn btn-dark btn-sm">Explore</a>
|
<a 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-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -112,53 +63,11 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Scrapbook: Maggie’s Designs</h5>
|
<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>
|
<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>
|
<a asp-action="MaggiesDesigns" class="btn btn-dark btn-sm">View Scrapbook</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
@section Meta{
|
@section Meta{
|
||||||
|
|||||||
165
CatherineLynwood/Views/Discovery/HowToBuy.cshtml
Normal file
165
CatherineLynwood/Views/Discovery/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>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,401 +0,0 @@
|
|||||||
@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>
|
|
||||||
|
|
||||||
<!-- Specific Row for Book Cover and Synopsis -->
|
|
||||||
<div class="row">
|
|
||||||
<!-- Book Cover Section -->
|
|
||||||
<div class="col-md-4">
|
|
||||||
<section id="book-cover">
|
|
||||||
<div class="card character-card" 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">The Front Cover</h3>
|
|
||||||
<p class="card-text">This is the final front cover of The Alpha Flame: Discovery. It features Maggie stood outside the derelict Rubery Hill Hospital.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (showReviews)
|
|
||||||
{
|
|
||||||
<!-- Buy Section -->
|
|
||||||
<div class="col-md-8">
|
|
||||||
<section id="purchase-and-reviews">
|
|
||||||
<div class="card character-card" id="companion-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h1>The Alpha Flame: <span class="fw-light">Discovery</span><br /><span class="h2">A Gritty 1980s Birmingham Crime Novel</span></h1>
|
|
||||||
<h2 class="h3">Survival, secrets, and sisters in 1980s Birmingham.</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="companion-body">
|
|
||||||
<div class="p-2">
|
|
||||||
<h2 class="h5">Buy the Book</h2>
|
|
||||||
</div>
|
|
||||||
<!-- Buy Now Section -->
|
|
||||||
<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>
|
|
||||||
<a id="paperbackLink" href="https://www.amazon.co.uk/dp/1068225815" target="_blank" class="btn btn-dark mb-2">
|
|
||||||
Buy Paperback (Bookshop Edition)
|
|
||||||
</a>
|
|
||||||
<a id="hardbackLink" href="https://www.amazon.co.uk/dp/1068225807" target="_blank" class="btn btn-dark mb-2">
|
|
||||||
Buy Hardback (Collector's Edition)
|
|
||||||
</a>
|
|
||||||
<p id="geoNote" class="text-muted small mt-2">
|
|
||||||
Available from your local Amazon store.<br />
|
|
||||||
Or order from your local bookshop using:
|
|
||||||
<ul class="small text-muted pl-3 mb-1">
|
|
||||||
<li>ISBN 978-1-0682258-1-9 – Bookshop Edition (Paperback)</li>
|
|
||||||
<li>ISBN 978-1-0682258-0-2 – Collector's Edition (Hardback)</li>
|
|
||||||
</ul>
|
|
||||||
<span id="extraRetailers"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Reader Reviews -->
|
|
||||||
<div class="reader-reviews">
|
|
||||||
<h3 class="h6 text-uppercase text-muted">★ Reader Praise ★</h3>
|
|
||||||
|
|
||||||
@foreach (var review in Model.Items.Take(3))
|
|
||||||
{
|
|
||||||
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");
|
|
||||||
|
|
||||||
<blockquote class="blockquote mb-4">
|
|
||||||
<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>
|
|
||||||
@review.AuthorName on <cite title="@review.SiteName">
|
|
||||||
@if (string.IsNullOrEmpty(review.URL))
|
|
||||||
{
|
|
||||||
@review.SiteName
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a href="@review.URL" target="_blank">@review.SiteName</a>
|
|
||||||
}
|
|
||||||
</cite> — <span class="text-muted smaller">@reviewDate</span>
|
|
||||||
</footer>
|
|
||||||
</blockquote>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Model.Items.Count > 3)
|
|
||||||
{
|
|
||||||
<div class="text-end">
|
|
||||||
<a asp-action="Reviews" class="btn btn-outline-secondary btn-sm">
|
|
||||||
Read More Reviews
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Synopsis Section -->
|
|
||||||
<div class="col-md-12">
|
|
||||||
<section id="synopsis">
|
|
||||||
<div class="card character-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2 class="card-title h1">The Alpha Flame: <span class="fw-light">Discovery:</span> Synopsis</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="synopsis-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<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">
|
|
||||||
<!-- Audio Section -->
|
|
||||||
<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">
|
|
||||||
Listen to Catherine telling you about The Alpha Flame: Discovery
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Synopsis Content -->
|
|
||||||
<h3 class="card-title">Synopsis</h3>
|
|
||||||
<p class="card-text">Set in 1983 Birmingham, The Alpha Flame: Discovery is a gritty crime novel following twin sisters Beth and Maggie as they uncover dark family secrets and fight to survive abuse in a harsh, realistic world. With unflinching honesty, it explores the bonds of family, the scars of the past, and the resilience needed to endure. This powerful first instalment in the trilogy immerses readers in the grim realities of 1980s Britain while celebrating hope in the face of darkness.</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 explores the devastating lows and triumphant highs of life with unflinching honesty, capturing 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 frame.</p>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<!-- Synopsis Section -->
|
|
||||||
<div class="col-md-8">
|
|
||||||
<section id="synopsis">
|
|
||||||
<div class="card character-card" id="companion-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h1>The Alpha Flame: <span class="fw-light">Discovery</span><br /><span class="h2">A Gritty 1980s Birmingham Crime Novel</span></h1>
|
|
||||||
<h2 class="h3">Survival, secrets, and sisters in 1980s Birmingham.</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="companion-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<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">
|
|
||||||
<!-- Audio Section -->
|
|
||||||
<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">
|
|
||||||
Listen to Catherine telling you about The Alpha Flame: Discovery
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div id="buy-now" class="my-4">
|
|
||||||
<a id="kindleLink" href="https://www.amazon.co.uk/dp/B0FBS427VD" target="_blank" class="btn btn-dark mb-2">
|
|
||||||
Buy Kindle Edition
|
|
||||||
</a>
|
|
||||||
<a id="paperbackLink" href="https://www.amazon.co.uk/dp/1068225815" target="_blank" class="btn btn-dark mb-2">
|
|
||||||
Buy Paperback (Bookshop Edition)
|
|
||||||
</a>
|
|
||||||
<a id="hardbackLink" href="https://www.amazon.co.uk/dp/1068225807" target="_blank" class="btn btn-dark mb-2">
|
|
||||||
Buy Hardback (Collector's Edition)
|
|
||||||
</a>
|
|
||||||
<p id="geoNote" class="text-muted small mt-2">
|
|
||||||
Available from your local Amazon store.<br />
|
|
||||||
Or order from your local bookshop using:
|
|
||||||
<ul class="small text-muted">
|
|
||||||
<li>
|
|
||||||
ISBN 978-1-0682258-1-9 - Bookshop Edition (Paperback)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
ISBN 978-1-0682258-0-2 - Collector's Eidtion (Hardback)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<span id="extraRetailers"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Synopsis Content -->
|
|
||||||
<h3 class="card-title">Synopsis</h3>
|
|
||||||
<p class="card-text">Set in 1983 Birmingham, The Alpha Flame: Discovery is a gritty crime novel following twin sisters Beth and Maggie as they uncover dark family secrets and fight to survive abuse in a harsh, realistic world. With unflinching honesty, it explores the bonds of family, the scars of the past, and the resilience needed to endure. This powerful first instalment in the trilogy immerses readers in the grim realities of 1980s Britain while celebrating hope in the face of darkness.</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 explores the devastating lows and triumphant highs of life with unflinching honesty, capturing 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 frame.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Giveway Section -->
|
|
||||||
@if (DateTime.Now < new DateTime(2025, 9, 1))
|
|
||||||
{
|
|
||||||
<div class="col-md-12 mt-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<h2 class="display-6 fw-bold">Win: <span class="fw-light">a Collector’s Edition of The Alpha Flame: Discovery</span></h2>
|
|
||||||
<p class="mb-2">
|
|
||||||
Enter my giveaway for your chance to own this special edition. Signed in the UK, or delivered as a premium collector’s copy worldwide.
|
|
||||||
</p>
|
|
||||||
<em>Exclusive, limited, beautiful.</em>
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-8 col-md-4 mt-4">
|
|
||||||
<a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">Enter now for your chance.</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chapter Previews Section -->
|
|
||||||
<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 {
|
|
||||||
<script>
|
|
||||||
window.addEventListener("load", function () {
|
|
||||||
const coverCard = document.getElementById("cover-card");
|
|
||||||
const companionCard = document.getElementById("companion-card");
|
|
||||||
const companionBody = document.getElementById("companion-body");
|
|
||||||
|
|
||||||
if (coverCard && companionCard && companionBody) {
|
|
||||||
// Match the height of the synopsis card to the cover card
|
|
||||||
const coverHeight = coverCard.offsetHeight;
|
|
||||||
companionCard.style.height = `${coverHeight}px`;
|
|
||||||
|
|
||||||
// Adjust the synopsis body to scroll within the matched height
|
|
||||||
const headerHeight = companionCard.querySelector(".card-header").offsetHeight;
|
|
||||||
companionBody.style.maxHeight = `${coverHeight - headerHeight}px`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const player = new Plyr('audio');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<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 = "";
|
|
||||||
|
|
||||||
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">Waterstons</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/9781068225810" 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("kindleLink").setAttribute("href", kindleLink);
|
|
||||||
document.getElementById("paperbackLink").setAttribute("href", paperbackLink);
|
|
||||||
document.getElementById("hardbackLink").setAttribute("href", hardbackLink);
|
|
||||||
document.getElementById("extraRetailers").innerHTML = extraRetailers;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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>
|
|
||||||
|
|
||||||
}
|
|
||||||
355
CatherineLynwood/Views/Discovery/IndexDesktop.cshtml
Normal file
355
CatherineLynwood/Views/Discovery/IndexDesktop.cshtml
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- HERO: Cover + Trailer + Buy Box -->
|
||||||
|
<section class="mb-4">
|
||||||
|
<div class="row g-3 align-items-stretch">
|
||||||
|
<!-- Book Cover -->
|
||||||
|
<div class="col-lg-5 d-flex d-none d-lg-block">
|
||||||
|
<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">It's 1983 Birmingham. Maggie, my fiery heroine, standing outside the derelict Rubery Hill Hospital. A story of survival, sisters, and fire.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trailer + Buy Box -->
|
||||||
|
<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">
|
||||||
|
<!-- Trailer -->
|
||||||
|
<div class="trailer-wrapper mb-3">
|
||||||
|
<!-- Desktop: LANDSCAPE -->
|
||||||
|
<video id="trailerLandscape"
|
||||||
|
class="w-100"
|
||||||
|
playsinline
|
||||||
|
preload="none"
|
||||||
|
poster="/images/webp/the-alpha-flame-discovery-trailer-landscape-1400.webp"
|
||||||
|
controls>
|
||||||
|
<source src="/videos/the-alpha-flame-discovery-trailer-landscape.mp4" type="video/mp4">
|
||||||
|
Sorry, your browser doesn't support embedded video.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<button id="trailerPlayBtn" class="trailer-play-btn">
|
||||||
|
<i class="fad fa-play"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If JS is off, hide the custom play button so native controls are used -->
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#trailerPlayBtn {
|
||||||
|
display: none !important
|
||||||
|
}</style>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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)
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- 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 shadows 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">
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Desktop-only: collapse trigger -->
|
||||||
|
<p class="mb-2 d-none d-md-block">
|
||||||
|
<a class="btn btn-outline-light btn-sm"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
href="#fullSynopsis"
|
||||||
|
role="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="fullSynopsis">
|
||||||
|
Read full synopsis
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- One copy of the full synopsis -->
|
||||||
|
<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. 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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Trailer play/pause via custom button or video click (desktop only) -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const v = document.getElementById("trailerLandscape");
|
||||||
|
const playBtn = document.getElementById("trailerPlayBtn");
|
||||||
|
if (!v || !playBtn) return;
|
||||||
|
|
||||||
|
// Hide native controls when JS is active; use custom button instead
|
||||||
|
v.controls = false;
|
||||||
|
|
||||||
|
// Start playback
|
||||||
|
const startPlayback = () => {
|
||||||
|
v.muted = false;
|
||||||
|
v.volume = 1.0;
|
||||||
|
v.play()
|
||||||
|
.then(() => { playBtn.style.display = "none"; })
|
||||||
|
.catch(err => console.warn("Video play failed:", err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle on video click
|
||||||
|
const togglePlayback = () => {
|
||||||
|
if (v.paused) {
|
||||||
|
startPlayback();
|
||||||
|
} else {
|
||||||
|
v.pause();
|
||||||
|
playBtn.style.display = "block";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Events
|
||||||
|
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
|
||||||
|
v.addEventListener("click", togglePlayback);
|
||||||
|
|
||||||
|
// Keep button state in sync with native events
|
||||||
|
v.addEventListener("play", () => { playBtn.style.display = "none"; });
|
||||||
|
v.addEventListener("pause", () => { playBtn.style.display = "block"; });
|
||||||
|
v.addEventListener("ended", () => { playBtn.style.display = "block"; });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const el = document.getElementById('fullSynopsis');
|
||||||
|
if (!el || !window.bootstrap?.Collapse) return;
|
||||||
|
|
||||||
|
// Initialise without auto-toggling
|
||||||
|
const c = new bootstrap.Collapse(el, { toggle: false });
|
||||||
|
const mq = window.matchMedia('(min-width: 768px)'); // Bootstrap md
|
||||||
|
|
||||||
|
function setInitial() {
|
||||||
|
if (mq.matches) {
|
||||||
|
// Desktop: keep collapsed
|
||||||
|
c.hide();
|
||||||
|
} else {
|
||||||
|
// Mobile: open by default
|
||||||
|
c.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitial();
|
||||||
|
// Optional: if user resizes across the breakpoint, adjust state
|
||||||
|
mq.addEventListener?.('change', setInitial);
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
}
|
||||||
308
CatherineLynwood/Views/Discovery/IndexMobileA.cshtml
Normal file
308
CatherineLynwood/Views/Discovery/IndexMobileA.cshtml
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
<div class="card character-card" id="hero-media-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Trailer -->
|
||||||
|
<div class="trailer-wrapper mb-3">
|
||||||
|
<!-- Mobile / tablet: PORTRAIT -->
|
||||||
|
<video id="trailerPortrait"
|
||||||
|
class="w-100"
|
||||||
|
playsinline
|
||||||
|
preload="none"
|
||||||
|
poster="/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp"
|
||||||
|
controls>
|
||||||
|
<source src="/videos/the-alpha-flame-discovery-trailer-portrait.mp4" type="video/mp4">
|
||||||
|
Sorry, your browser doesn't support embedded video.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<button id="trailerPlayBtn" class="trailer-play-btn">
|
||||||
|
<i class="fad fa-play"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If JS is off, hide the custom play button so native controls are used -->
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#trailerPlayBtn {
|
||||||
|
display: none !important
|
||||||
|
}</style>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src })
|
||||||
|
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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)
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- 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">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>
|
||||||
|
</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 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>
|
||||||
|
|
||||||
|
<!-- Trailer play/pause via custom button or video click (single video) -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const v = document.getElementById("trailerPortrait");
|
||||||
|
const playBtn = document.getElementById("trailerPlayBtn");
|
||||||
|
if (!v || !playBtn) return;
|
||||||
|
|
||||||
|
// Hide native controls when JS is active; use custom button instead
|
||||||
|
v.controls = false;
|
||||||
|
|
||||||
|
// Start playback
|
||||||
|
const startPlayback = () => {
|
||||||
|
v.muted = false;
|
||||||
|
v.volume = 1.0;
|
||||||
|
v.play()
|
||||||
|
.then(() => { playBtn.style.display = "none"; })
|
||||||
|
.catch(err => console.warn("Video play failed:", err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle on video click
|
||||||
|
const togglePlayback = () => {
|
||||||
|
if (v.paused) {
|
||||||
|
startPlayback();
|
||||||
|
} else {
|
||||||
|
v.pause();
|
||||||
|
playBtn.style.display = "block";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Events
|
||||||
|
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
|
||||||
|
v.addEventListener("click", togglePlayback);
|
||||||
|
|
||||||
|
// Keep button state in sync with native events
|
||||||
|
v.addEventListener("play", () => { playBtn.style.display = "none"; });
|
||||||
|
v.addEventListener("pause", () => { playBtn.style.display = "block"; });
|
||||||
|
v.addEventListener("ended", () => { playBtn.style.display = "block"; });
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
}
|
||||||
309
CatherineLynwood/Views/Discovery/IndexMobileB.cshtml
Normal file
309
CatherineLynwood/Views/Discovery/IndexMobileB.cshtml
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- HERO: Cover + Trailer (no Buy Box here in Version B) -->
|
||||||
|
<section class="mb-4">
|
||||||
|
<div class="row g-3 align-items-stretch">
|
||||||
|
<!-- Trailer only -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card character-card" id="hero-media-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Trailer -->
|
||||||
|
<div class="trailer-wrapper mb-3">
|
||||||
|
<!-- Mobile / tablet: PORTRAIT -->
|
||||||
|
<video id="trailerPortrait"
|
||||||
|
class="w-100"
|
||||||
|
playsinline
|
||||||
|
preload="none"
|
||||||
|
poster="/images/webp/the-alpha-flame-discovery-trailer-portrait-400.webp"
|
||||||
|
controls>
|
||||||
|
<source src="/videos/the-alpha-flame-discovery-trailer-portrait.mp4" type="video/mp4">
|
||||||
|
Sorry, your browser doesn't support embedded video.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<button id="trailerPlayBtn" class="trailer-play-btn">
|
||||||
|
<i class="fad fa-play"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If JS is off, hide the custom play button so native controls are used -->
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
#trailerPlayBtn {
|
||||||
|
display: none !important
|
||||||
|
}</style>
|
||||||
|
</noscript>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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;">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</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">
|
||||||
|
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src })
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Trailer play/pause via custom button or video click (single video) -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const v = document.getElementById("trailerPortrait");
|
||||||
|
const playBtn = document.getElementById("trailerPlayBtn");
|
||||||
|
if (!v || !playBtn) return;
|
||||||
|
|
||||||
|
// Hide native controls when JS is active; use custom button instead
|
||||||
|
v.controls = false;
|
||||||
|
|
||||||
|
// Start playback
|
||||||
|
const startPlayback = () => {
|
||||||
|
v.muted = false;
|
||||||
|
v.volume = 1.0;
|
||||||
|
v.play()
|
||||||
|
.then(() => { playBtn.style.display = "none"; })
|
||||||
|
.catch(err => console.warn("Video play failed:", err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle on video click
|
||||||
|
const togglePlayback = () => {
|
||||||
|
if (v.paused) {
|
||||||
|
startPlayback();
|
||||||
|
} else {
|
||||||
|
v.pause();
|
||||||
|
playBtn.style.display = "block";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Events
|
||||||
|
playBtn.addEventListener("click", (e) => { e.preventDefault(); startPlayback(); });
|
||||||
|
v.addEventListener("click", togglePlayback);
|
||||||
|
|
||||||
|
// Keep button state in sync with native events
|
||||||
|
v.addEventListener("play", () => { playBtn.style.display = "none"; });
|
||||||
|
v.addEventListener("pause", () => { playBtn.style.display = "block"; });
|
||||||
|
v.addEventListener("ended", () => { playBtn.style.display = "block"; });
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@
|
|||||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
<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="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="Index">Discovery</a></li>
|
||||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Listen</li>
|
<li class="breadcrumb-item active" aria-current="page">Listen</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
<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="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="Index">Discovery</a></li>
|
||||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Maggie's Designs</li>
|
<li class="breadcrumb-item active" aria-current="page">Maggie's Designs</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<li class="breadcrumb-item"><a asp-controller="Home" asp-action="Index">Home</a></li>
|
<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="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="Index">Discovery</a></li>
|
||||||
<li class="breadcrumb-item"><a asp-controller="Discovery" asp-action="Extras">Extras</a></li>
|
<li class="breadcrumb-item"><a asp-controller="TheAlphaFlame" asp-action="Extras">Extras</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Discovery Scrap Book</li>
|
<li class="breadcrumb-item active" aria-current="page">Discovery Scrap Book</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
198
CatherineLynwood/Views/Discovery/Soundtrack.cshtml
Normal file
198
CatherineLynwood/Views/Discovery/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/Discovery/Trailer.cshtml
Normal file
488
CatherineLynwood/Views/Discovery/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>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
28
CatherineLynwood/Views/Home/ARCThanks.cshtml
Normal file
28
CatherineLynwood/Views/Home/ARCThanks.cshtml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Thank You";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container my-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10 col-lg-8 text-center">
|
||||||
|
<h1 class="display-5 fw-bold mb-4">Thank You for Applying!</h1>
|
||||||
|
<p class="lead">I'm so grateful you've offered to read <em>The Alpha Flame: Discovery</em> and consider reviewing it. It really means a lot.</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info mt-4 text-start">
|
||||||
|
<h5 class="fw-semibold">📩 Please check your inbox</h5>
|
||||||
|
<p class="mb-2">I've just sent you an email with setup instructions based on how you said you'd like to receive the book — whether via Kindle or an alternative method.</p>
|
||||||
|
<p class="mb-2">If it doesn't appear soon, please check your spam or junk folder. If it’s not there either, feel free to contact me directly.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning text-start">
|
||||||
|
<h5 class="fw-semibold">✅ One last step...</h5>
|
||||||
|
<p>Once you've completed the setup (or if you need help), please reply to the email and let me know. That way I can get the ARC sent out to you right away.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4">Thanks again for your support — it genuinely makes a difference.</p>
|
||||||
|
<p class="fst-italic">Warmest wishes,<br>Catherine</p>
|
||||||
|
|
||||||
|
<a asp-action="Index" class="btn btn-primary mt-3">Return to Home Page</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<p class="mb-0"><em>For <strong>The Alpha Flame: Discovery</strong> – Advance Reader Copy (ARC)</em></p>
|
<p class="mb-0"><em>For <strong>The Alpha Flame: Discovery</strong> – Advance Reader Copy (ARC)</em></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-action="ArcReaderApplication" id="arcWizardForm" novalidate method="post">
|
<form asp-action="ArcReaderApplication" id="arcWizardForm" novalidate method="post" onsubmit="disableSubmit(this)">
|
||||||
<div class="progress mb-4" style="height: 1.5rem;">
|
<div class="progress mb-4" style="height: 1.5rem;">
|
||||||
<div id="arcWizardProgress" class="progress-bar bg-success" style="width: 20%;">
|
<div id="arcWizardProgress" class="progress-bar bg-success" style="width: 20%;">
|
||||||
Step 1 of 5
|
Step 1 of 5
|
||||||
@ -23,34 +23,31 @@
|
|||||||
<section class="arc-intro">
|
<section class="arc-intro">
|
||||||
<h1 class="display-5"><i class="fad fa-book-reader me-2 text-primary"></i>Become an ARC Reader</h1>
|
<h1 class="display-5"><i class="fad fa-book-reader me-2 text-primary"></i>Become an ARC Reader</h1>
|
||||||
<p class="lead">Fancy reading <strong>The Alpha Flame: Discovery</strong> before anyone else? I'm looking for passionate early readers to receive a free Kindle copy in exchange for an honest review.</p>
|
<p class="lead">Fancy reading <strong>The Alpha Flame: Discovery</strong> before anyone else? I'm looking for passionate early readers to receive a free Kindle copy in exchange for an honest review.</p>
|
||||||
|
<button type="button" class="btn btn-dark arc-next mb-3">Start Application</button>
|
||||||
|
|
||||||
<div class="alert alert-warning mt-4">
|
<p>
|
||||||
<i class="fad fa-exclamation-triangle me-2"></i>
|
The Alpha Flame: Discovery is a gritty, character-driven crime novel set in 1983 Birmingham. Beth and Maggie are thrown together by fate, each carrying trauma, secrets, and fire. Beth is a survivor — wounded, wary, and haunted by the past. Maggie is bold, passionate, and dangerous to underestimate. Their bond is raw, explosive, and deeply human. Together, they must face a world that wants to break them — and fight back harder.
|
||||||
<strong>Note:</strong> This novel is raw and emotional. Please check the themes below.
|
</p>
|
||||||
</div>
|
<p>
|
||||||
<h2 class="h5 mt-4"><i class="fad fa-tags me-2 text-secondary"></i>Major Themes and Topics</h2>
|
This is a story of survival, sisterhood, and power. Unflinching in its honesty, The Alpha Flame explores abuse, love, identity, and the fragile strength that carries us through the darkest nights. With fast cars, dangerous men, and high-stakes emotion set against the electric backdrop of 1980s Britain, this is not a soft read — but it burns with hope, truth, and fierce female energy.
|
||||||
<ul class="fa-ul">
|
</p>
|
||||||
<li><span class="fa-li"><i class="fad fa-skull-crossbones text-danger"></i></span> Death, trauma, and grief through the eyes of a teenage girl</li>
|
<p>
|
||||||
<li><span class="fa-li"><i class="fad fa-hand-rock text-danger"></i></span> Physical and sexual abuse (non-graphic, but deeply affecting)</li>
|
If you'd like to know more then take a look at the main book page.
|
||||||
<li><span class="fa-li"><i class="fad fa-procedures text-muted"></i></span> Mental health, including suicidal thoughts and PTSD</li>
|
</p>
|
||||||
<li><span class="fa-li"><i class="fad fa-money-bill-wave text-warning"></i></span> Prostitution, exploitation, and coercion</li>
|
<p>
|
||||||
<li><span class="fa-li"><i class="fad fa-bomb text-danger"></i></span> Violence against women (threats, assaults, murder)</li>
|
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore the Book</a>
|
||||||
<li><span class="fa-li"><i class="fad fa-hand-holding-heart text-success"></i></span> Love, trust, tenderness, and emotional recovery</li>
|
</p>
|
||||||
<li><span class="fa-li"><i class="fad fa-transgender text-info"></i></span> Sexuality, orientation, and identity discovery</li>
|
|
||||||
<li><span class="fa-li"><i class="fad fa-user-friends text-success"></i></span> Found family, sisterhood, and female empowerment</li>
|
|
||||||
<li><span class="fa-li"><i class="fad fa-mask text-dark"></i></span> Corruption, manipulation, and cover-ups</li>
|
|
||||||
<li><span class="fa-li"><i class="fad fa-gavel text-dark"></i></span> Justice, revenge, and difficult moral choices</li>
|
|
||||||
<li><span class="fa-li"><i class="fad fa-music text-secondary"></i></span> Music and fashion as creative expression and survival</li>
|
|
||||||
</ul>
|
|
||||||
<p class="mt-4">If you’re still interested, I’d love to have you on board. ARC readers help spread the word and offer early feedback that matters.</p>
|
<p class="mt-4">If you’re still interested, I’d love to have you on board. ARC readers help spread the word and offer early feedback that matters.</p>
|
||||||
<p><strong>All I ask:</strong> read the book and leave a review on Goodreads, Amazon, or wherever you normally post.</p>
|
<p><strong>All I ask:</strong> read the book and leave a review on Goodreads, Amazon, or wherever you normally post.</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-5 border border-3 border-dark" 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>
|
<button type="button" class="btn btn-link arc-next">
|
||||||
|
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-5 border border-3 border-dark" 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>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
<button type="button" class="btn btn-dark arc-next">Start Application</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="arc-step d-none" style="min-height: 65vh;" data-step="2">
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="2">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -66,61 +63,56 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Email"></label>
|
<label asp-for="Email"></label>
|
||||||
<input asp-for="Email" class="form-control" />
|
<input asp-for="Email" class="form-control" />
|
||||||
<small class="form-text text-muted">This is where I’ll send updates and reminders.</small>
|
<small class="form-text text-muted">This is where I’ll send updates and reminders, and generally keep in touch.</small>
|
||||||
<span asp-validation-for="Email" class="text-danger"></span>
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
<button type="button" class="btn btn-dark arc-next">Continue</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="arc-step d-none" style="min-height: 65vh;" data-step="3">
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9">
|
<div class="col-md-12">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<p>ARCs are sent via Kindle only. You can use the free Kindle app or a Kindle device.</p>
|
<p><strong>How will you receive the ARC?</strong></p>
|
||||||
<p>Add the following sender to your Amazon Kindle Approved Senders list: <strong id="arc-email">[enable JavaScript to view]</strong></p>
|
<p>To protect the unpublished book from piracy, I send all ARC copies securely via Kindle. This sends the book directly to your Kindle app or device — no files to download, nothing to forward.</p>
|
||||||
<p>
|
<p><strong>You don’t need a physical Kindle.</strong> The free Kindle app works on phones, tablets, and desktops.</p>
|
||||||
To find your kindle email follow this link <a href="https://www.amazon.co.uk/myk" target="_blank">amazon.co.uk/myk</a>.
|
<p class="mt-3">Just let me know below if you have Kindle access. I’ll email you simple step-by-step instructions to get everything set up.</p>
|
||||||
Once there click on Preferences and then scroll down to Personal Document Settings, as shown in the screen shot.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="KindleEmail"></label>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<input asp-for="KindleEmail" class="form-control" />
|
|
||||||
<span class="input-group-text">@@kindle.com</span>
|
|
||||||
</div>
|
|
||||||
<span asp-validation-for="KindleEmail" class="text-danger"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Kindle Access -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="ApprovedSender"></label>
|
<label asp-for="HasKindleAccess" class="form-label"></label>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="Yes" id="ApprovedYes" />
|
<input class="form-check-input" type="radio" asp-for="HasKindleAccess" value="Yes" id="HasKindleYes" />
|
||||||
<label class="form-check-label" for="ApprovedYes">Yes — I’ve added your email</label>
|
<label class="form-check-label" for="HasKindleYes">Yes — I can receive it by Kindle</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="NotYet" id="ApprovedNotYet" />
|
<input class="form-check-input" type="radio" asp-for="HasKindleAccess" value="No" id="HasKindleNo" />
|
||||||
<label class="form-check-label" for="ApprovedNotYet">Not yet — but I will</label>
|
<label class="form-check-label" for="HasKindleNo">No — I’ll need an alternative method</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<span asp-validation-for="HasKindleAccess" class="text-danger"></span>
|
||||||
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="NeedHelp" id="ApprovedNeedHelp" />
|
</div>
|
||||||
<label class="form-check-label" for="ApprovedNeedHelp">I need help</label>
|
|
||||||
</div>
|
<!-- Optional Kindle Email -->
|
||||||
<span asp-validation-for="ApprovedSender" class="text-danger"></span>
|
<div class="form-group mt-3">
|
||||||
|
<label asp-for="KindleEmail">Your Kindle Email (optional)</label>
|
||||||
|
<input asp-for="KindleEmail" class="form-control" placeholder="e.g. yourname@kindle.com" />
|
||||||
|
<span asp-validation-for="KindleEmail" class="text-danger"></span>
|
||||||
|
<small class="form-text text-muted">If you know your Kindle email address already, pop it in here. Otherwise I’ll help you via email.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<responsive-image src="kindle-setup.png" class="img-fluid rounded-5 border border-3 border-dark" alt="Kindle setup screen shot" display-width-percentage="50"></responsive-image>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
<button type="button" class="btn btn-dark arc-next">Continue</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="arc-step d-none" style="min-height: 65vh;" data-step="4">
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -168,7 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-3">
|
<div class="col-12 mt-3">
|
||||||
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
<button type="button" class="btn btn-dark arc-next">Continue</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,7 +169,10 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<!-- Content Fit -->
|
<!-- Content Fit -->
|
||||||
<div class="alert alert-info mt-3">
|
<div class="alert alert-info mt-3">
|
||||||
I'm interested in the type of fiction you enjoy reading. I write what I describe as <a asp-controller="Home" asp-action="VerosticGenre" class="alert-link">verostic fiction</a>, I've even got a page on this website describing it.
|
I'm interested in the type of fiction you enjoy reading.
|
||||||
|
I write what I describe as <a asp-controller="Home" asp-action="VerosticGenre" asp-route-step="5" class="alert-link">verostic fiction</a>,
|
||||||
|
it's A literary genre or descriptive tone characterised by raw emotional realism, unflinching psychological depth, and grounded human truth.
|
||||||
|
Often gritty, sometimes painful, but always sincere.
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="ContentFit"></label>
|
<label asp-for="ContentFit"></label>
|
||||||
@ -263,6 +258,16 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function disableSubmit(form) {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = 'Sending...'; // Optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const steps = document.querySelectorAll('.arc-step');
|
const steps = document.querySelectorAll('.arc-step');
|
||||||
@ -288,7 +293,6 @@
|
|||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
// This uses jQuery Unobtrusive Validation to validate Razor-bound fields
|
|
||||||
if (!$(input).valid()) {
|
if (!$(input).valid()) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
@ -297,6 +301,57 @@
|
|||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore values
|
||||||
|
document.querySelectorAll('input, textarea, select').forEach(el => {
|
||||||
|
const name = el.name;
|
||||||
|
|
||||||
|
if (el.type === 'checkbox') {
|
||||||
|
const stored = localStorage.getItem(name);
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const values = JSON.parse(stored);
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
el.checked = values.includes(el.value);
|
||||||
|
} else {
|
||||||
|
el.checked = stored === 'true';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
el.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (el.type === 'radio') {
|
||||||
|
const stored = localStorage.getItem(name);
|
||||||
|
if (stored && el.value === stored) {
|
||||||
|
el.checked = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const stored = localStorage.getItem(name);
|
||||||
|
if (stored !== null) {
|
||||||
|
el.value = stored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save values
|
||||||
|
document.querySelectorAll('input, textarea, select').forEach(el => {
|
||||||
|
el.addEventListener('change', () => {
|
||||||
|
const name = el.name;
|
||||||
|
|
||||||
|
if (el.type === 'checkbox') {
|
||||||
|
const checkboxes = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`);
|
||||||
|
const checkedValues = Array.from(checkboxes)
|
||||||
|
.filter(cb => cb.checked)
|
||||||
|
.map(cb => cb.value);
|
||||||
|
localStorage.setItem(name, JSON.stringify(checkedValues));
|
||||||
|
} else if (el.type === 'radio') {
|
||||||
|
if (el.checked) {
|
||||||
|
localStorage.setItem(name, el.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(name, el.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.arc-next').forEach(btn => {
|
document.querySelectorAll('.arc-next').forEach(btn => {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
@ -321,7 +376,6 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Try to find first step with invalid field
|
|
||||||
for (let i = 0; i < steps.length; i++) {
|
for (let i = 0; i < steps.length; i++) {
|
||||||
const stepFields = steps[i].querySelectorAll('input, select, textarea');
|
const stepFields = steps[i].querySelectorAll('input, select, textarea');
|
||||||
for (const field of stepFields) {
|
for (const field of stepFields) {
|
||||||
@ -336,9 +390,16 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🌟 NEW: Handle "step" in URL query string
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const stepFromUrl = parseInt(urlParams.get('step'));
|
||||||
|
if (stepFromUrl && stepFromUrl > 0 && stepFromUrl <= steps.length) {
|
||||||
|
currentStep = stepFromUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial step display
|
||||||
showStep(currentStep);
|
showStep(currentStep);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -12,17 +12,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center align-items-center mb-5">
|
||||||
<div class="row align-items-center mb-5">
|
|
||||||
<div class="col-md-5 text-center">
|
<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">
|
<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 -->
|
<!-- Source will be injected later -->
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<!-- Overlay image remains untouched -->
|
<!-- 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"
|
class="hero-overlay-image"
|
||||||
alt="The Alpha Flame: Discovery by Catherine Lynwood"
|
alt="The Alpha Flame: Discovery by Catherine Lynwood"
|
||||||
display-width-percentage="50"
|
display-width-percentage="50"
|
||||||
@ -33,34 +32,88 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-7 pt-2 pt-md-0">
|
<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>
|
<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>
|
||||||
|
|
||||||
<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...
|
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>
|
</p>
|
||||||
<div class="d-flex gap-3 flex-wrap">
|
|
||||||
<a asp-controller="Discovery" asp-action="Index" class="btn btn-dark">Explore the Book</a>
|
<hr class="my-4" />
|
||||||
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
|
|
||||||
|
<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="TheAlphaFlame" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
|
||||||
</div>
|
</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.
|
<h2 class="h4 fw-bold">
|
||||||
The journey continues in <strong>Reckoning</strong> (Spring 2026) and concludes with <strong>Redemption</strong> (Autumn 2026).
|
The Alpha Flame: <span class="fw-light">Reckoning</span>
|
||||||
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>
|
||||||
|
|
||||||
|
<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>
|
</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))
|
@if (DateTime.Now < new DateTime(2025, 9, 1))
|
||||||
{
|
{
|
||||||
<p class="mt-4">
|
<hr class="my-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>
|
<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>
|
</p>
|
||||||
|
|
||||||
<div class="d-flex gap-3 flex-wrap mt-4">
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<p class="mt-4">
|
<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>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -72,6 +125,7 @@
|
|||||||
meta-author="Catherine Lynwood"
|
meta-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/"
|
meta-url="https://www.catherinelynwood.com/"
|
||||||
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-600.webp"
|
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-600.webp"
|
||||||
|
meta-image-png="https://www.catherinelynwood.com/images/the-alpha-flame-discovery-cover.png"
|
||||||
meta-image-alt="Cover artwork from 'The Alpha Flame: Discovery' by Catherine Lynwood"
|
meta-image-alt="Cover artwork from 'The Alpha Flame: Discovery' by Catherine Lynwood"
|
||||||
og-site-name="Catherine Lynwood – The Alpha Flame"
|
og-site-name="Catherine Lynwood – The Alpha Flame"
|
||||||
article-published-time="@new DateTime(2024, 11, 20)"
|
article-published-time="@new DateTime(2024, 11, 20)"
|
||||||
@ -191,7 +245,7 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const video = document.getElementById("heroVideo");
|
const video = document.getElementById("heroVideo");
|
||||||
const source = document.createElement("source");
|
const source = document.createElement("source");
|
||||||
source.src = "/videos/background-5.mp4";
|
source.src = "/videos/the-alpha-flame-reckoning.mp4";
|
||||||
source.type = "video/mp4";
|
source.type = "video/mp4";
|
||||||
video.appendChild(source);
|
video.appendChild(source);
|
||||||
video.load(); // Triggers actual file load
|
video.load(); // Triggers actual file load
|
||||||
|
|||||||
@ -13,6 +13,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (ViewData["step"] != null)
|
||||||
|
{
|
||||||
|
var step = ViewData["step"];
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<a class="btn btn-dark mt-4" asp-controller="Home" asp-action="ArcReaderApplication" asp-route-step="@step">
|
||||||
|
<i class="fad fa-arrow-left me-1"></i> Back to Application
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<section class="container py-2">
|
<section class="container py-2">
|
||||||
<h1 class="display-4 mb-4">The Verostic Genre</h1>
|
<h1 class="display-4 mb-4">The Verostic Genre</h1>
|
||||||
|
|
||||||
|
|||||||
335
CatherineLynwood/Views/IndieAuthor/AiForAuthors.cshtml
Normal file
335
CatherineLynwood/Views/IndieAuthor/AiForAuthors.cshtml
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "AI for Authors";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">AI for Authors</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">AI for authors, practical use without losing your soul</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
AI has become one of the biggest talking points in writing and publishing, which is hardly surprising.
|
||||||
|
It can generate text, create images, read words aloud, suggest ideas, and generally poke its nose into
|
||||||
|
almost every creative workflow going. That does not mean authors need to hand over the keys to the kingdom.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page looks at how AI can be used as a practical tool for indie authors, from brainstorming and
|
||||||
|
image inspiration to editing support and voice playback, while keeping the actual writing, judgement
|
||||||
|
and creative direction firmly in human hands where they belong.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="what-ai-is-good-for" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What AI is actually good for</h2>
|
||||||
|
<p>
|
||||||
|
AI is often most useful when it is treated as an assistant rather than an author. It can help
|
||||||
|
generate options, organise thoughts, test ideas, summarise information, and speed up certain
|
||||||
|
repetitive tasks.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Used sensibly, it can be helpful for:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>brainstorming ideas</li>
|
||||||
|
<li>testing scene descriptions</li>
|
||||||
|
<li>creating image inspiration</li>
|
||||||
|
<li>reading text aloud with synthetic voices</li>
|
||||||
|
<li>spotting awkward phrasing or repetition</li>
|
||||||
|
<li>helping structure notes and workflows</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
It is strongest when used to support your process, not replace your creative brain.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-ai-is-bad-for" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What AI is bad for</h2>
|
||||||
|
<p>
|
||||||
|
AI can sound convincing while being wrong, bland, generic, or weirdly overconfident. It also has
|
||||||
|
a strong tendency to produce text that looks smooth on the surface while lacking depth, originality
|
||||||
|
and genuine emotional intelligence.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That makes it a poor replacement for:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>the core writing of a novel</li>
|
||||||
|
<li>authentic voice and character work</li>
|
||||||
|
<li>subtle emotional judgement</li>
|
||||||
|
<li>deep originality</li>
|
||||||
|
<li>final creative decision-making</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
In other words, it can assist with craft and process, but it is not your imagination and should not be mistaken for it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="brainstorming-and-idea-testing" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Brainstorming and testing ideas</h2>
|
||||||
|
<p>
|
||||||
|
One of the best uses of AI for authors is as a thinking partner when you are trying to clarify an idea.
|
||||||
|
That might mean exploring plot possibilities, testing alternate scene directions, or asking for suggestions
|
||||||
|
when something in the story feels thin or unclear.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The value here is not that the AI magically produces the perfect answer. It is that it can throw possibilities
|
||||||
|
back at you quickly, helping you react, reject, refine and sharpen your own thoughts.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Sometimes the most useful AI answer is the one that makes you realise what you do not want.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ai-for-editing-support" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Using AI to support editing</h2>
|
||||||
|
<p>
|
||||||
|
AI can be useful during editing, especially when you want another angle on the text. It can help identify
|
||||||
|
repetition, flag clunky phrasing, suggest places that may need tightening, or help summarise structural issues
|
||||||
|
you are already circling around.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That said, its suggestions still need judgement. AI is perfectly capable of recommending changes that flatten
|
||||||
|
style, simplify voice, or remove exactly the thing that made the writing interesting in the first place.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The trick is to use it as a filter for possibilities, not as a final editor issuing commandments from a silicon mountaintop.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ai-voice-playback" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">AI voice playback and hearing the text</h2>
|
||||||
|
<p>
|
||||||
|
One of the most practical uses of AI for authors is synthetic voice playback. Hearing your words read aloud
|
||||||
|
can reveal problems you simply do not notice while reading silently.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It can help expose:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>repetition</li>
|
||||||
|
<li>missing words</li>
|
||||||
|
<li>awkward rhythm</li>
|
||||||
|
<li>unnatural dialogue</li>
|
||||||
|
<li>sentences that look fine but sound dreadful</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
This can be especially useful because your own eyes are too forgiving. They know what you meant to write and
|
||||||
|
often glide right over the bits that need fixing.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The synthetic voice creates just enough distance to let you hear the work more honestly.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ai-images-for-inspiration" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Using AI images for inspiration</h2>
|
||||||
|
<p>
|
||||||
|
AI image tools can be genuinely helpful when you are trying to visualise a character, a location, a mood,
|
||||||
|
or the atmosphere of a scene. Sometimes putting a description into an image model helps clarify what you have
|
||||||
|
been trying to picture all along.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This can be useful for:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>character inspiration</li>
|
||||||
|
<li>setting exploration</li>
|
||||||
|
<li>cover concept mood boards</li>
|
||||||
|
<li>marketing ideas</li>
|
||||||
|
<li>checking whether a description produces the feel you intended</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Used this way, AI imagery is less about replacing art and more about helping ideas become visible.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ai-for-cover-concepts" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">AI in the cover design process</h2>
|
||||||
|
<p>
|
||||||
|
AI can also play a role in cover development, not necessarily as the final cover, but as a way to test mood,
|
||||||
|
composition, symbolism and visual direction before doing the serious design work.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It is useful for rough concepts because it is fast. You can try multiple directions without spending hours
|
||||||
|
building each one from scratch. That speed makes it good for exploration.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The danger is confusing a striking AI image with a functional book cover. A cover still needs typography,
|
||||||
|
hierarchy, genre signalling and all the boring practical bits that actually help sell books.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ai-for-marketing-and-admin" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">AI for marketing and admin support</h2>
|
||||||
|
<p>
|
||||||
|
Beyond writing and visuals, AI can be handy for the more practical side of author life. It can help draft
|
||||||
|
blog outlines, summarise ideas for newsletters, suggest website structures, help organise launch plans,
|
||||||
|
or turn messy thoughts into something more structured.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is often where AI shines because these tasks benefit from speed and structure without demanding that
|
||||||
|
the tool be the true creative source.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
It is much easier to let AI help with the admin jungle than to trust it with the heart of your novel.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ethical-and-creative-concerns" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Ethical and creative concerns</h2>
|
||||||
|
<p>
|
||||||
|
AI does raise real questions, and authors are right to think about them seriously. Concerns often include
|
||||||
|
originality, training data, authorship, transparency, and the risk of flooding the market with low-effort work.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
There is a meaningful difference between using AI as a practical support tool and using it to mass-produce
|
||||||
|
hollow content while pretending it is the same as real creative labour.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Authors should think carefully about where they draw their own line and be honest about how they use these tools.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="common-mistakes-with-ai" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Common mistakes authors make with AI</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>treating AI output as final rather than provisional</li>
|
||||||
|
<li>allowing it to flatten personal voice</li>
|
||||||
|
<li>using it to generate generic prose and calling it creativity</li>
|
||||||
|
<li>trusting facts or claims without checking them</li>
|
||||||
|
<li>mistaking speed for quality</li>
|
||||||
|
<li>letting the tool dictate the project instead of serving it</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="a-sensible-approach" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">A sensible way to approach AI</h2>
|
||||||
|
<p>
|
||||||
|
The most useful mindset is probably this: let AI help with the bits that benefit from speed, iteration,
|
||||||
|
and alternate perspectives, but keep the core creative work under human control.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That means using it to support:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>idea exploration</li>
|
||||||
|
<li>editing passes</li>
|
||||||
|
<li>voice playback</li>
|
||||||
|
<li>visual inspiration</li>
|
||||||
|
<li>workflow organisation</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
It does not mean handing over authorship and hoping the machine accidentally produces a soul.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-ai" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on AI for authors</h2>
|
||||||
|
<p>
|
||||||
|
I think AI is a useful tool when kept in its proper place. It can support creativity, sharpen process,
|
||||||
|
speed up exploration, and help authors catch things they might otherwise miss. That is valuable.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
But I do not think it replaces actual writing, actual judgement, or actual artistic intent. The danger is
|
||||||
|
not that AI exists. The danger is that people start mistaking convenience for craft.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Used properly, AI can make an author more effective. Used lazily, it just makes the work emptier faster.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#what-ai-is-good-for" class="text-decoration-none">What AI is good for</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-ai-is-bad-for" class="text-decoration-none">What AI is bad for</a></li>
|
||||||
|
<li class="mb-2"><a href="#brainstorming-and-idea-testing" class="text-decoration-none">Brainstorming and ideas</a></li>
|
||||||
|
<li class="mb-2"><a href="#ai-for-editing-support" class="text-decoration-none">Editing support</a></li>
|
||||||
|
<li class="mb-2"><a href="#ai-voice-playback" class="text-decoration-none">AI voice playback</a></li>
|
||||||
|
<li class="mb-2"><a href="#ai-images-for-inspiration" class="text-decoration-none">AI images for inspiration</a></li>
|
||||||
|
<li class="mb-2"><a href="#ai-for-cover-concepts" class="text-decoration-none">AI for cover concepts</a></li>
|
||||||
|
<li class="mb-2"><a href="#ai-for-marketing-and-admin" class="text-decoration-none">Marketing and admin</a></li>
|
||||||
|
<li class="mb-2"><a href="#ethical-and-creative-concerns" class="text-decoration-none">Ethical concerns</a></li>
|
||||||
|
<li class="mb-2"><a href="#common-mistakes-with-ai" class="text-decoration-none">Common mistakes</a></li>
|
||||||
|
<li class="mb-2"><a href="#a-sensible-approach" class="text-decoration-none">A sensible approach</a></li>
|
||||||
|
<li><a href="#my-view-on-ai" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
AI is useful as a support tool for ideas, editing, images and workflow, but it is not a substitute
|
||||||
|
for voice, judgement or genuine creative intent.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="EditingAndProofreading" class="text-decoration-none">
|
||||||
|
Editing and Proofreading
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="text-decoration-none">
|
||||||
|
Cover Design
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="text-decoration-none">
|
||||||
|
Traditional vs Self Publishing
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="btn btn-dark">
|
||||||
|
Previous: Author Finances
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index" class="btn btn-dark">
|
||||||
|
Back to guide index
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
360
CatherineLynwood/Views/IndieAuthor/AmazonAdvertising.cshtml
Normal file
360
CatherineLynwood/Views/IndieAuthor/AmazonAdvertising.cshtml
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Amazon Advertising for Indie Authors";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Amazon Advertising</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Amazon Advertising for indie authors</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
Amazon Ads can be one of the most obvious ways to get your book in front of readers, and one of
|
||||||
|
the easiest ways to burn through money while feeling oddly productive. The platform makes it look
|
||||||
|
wonderfully simple... set a bid, choose some targets, launch a campaign, and wait for readers to
|
||||||
|
appear. Real life is a bit less romantic.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page looks at how Amazon advertising works for indie authors, the different campaign types,
|
||||||
|
pay-per-click costs, why a single book often struggles to make advertising profitable, and why
|
||||||
|
the long-term value usually comes from building a catalogue rather than expecting one title to
|
||||||
|
carry the entire business on its back.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="what-amazon-ads-are" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What Amazon Ads actually are</h2>
|
||||||
|
<p>
|
||||||
|
Amazon advertising allows you to pay for placement inside Amazon’s own shopping ecosystem.
|
||||||
|
Your book can appear in search results, on product pages, and in other positions where
|
||||||
|
potential readers might see it while browsing.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The core idea is simple. You bid for visibility. If somebody clicks your ad, you pay.
|
||||||
|
Whether that click turns into a sale is another matter entirely.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
That means advertising is not really about buying sales directly. It is about paying for
|
||||||
|
the chance to be seen.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="pay-per-click" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Pay per click, not pay per sale</h2>
|
||||||
|
<p>
|
||||||
|
This is the first thing authors need to understand properly. Amazon Ads are generally
|
||||||
|
<strong>pay per click</strong>. You pay when somebody clicks the ad, not when they buy
|
||||||
|
the book.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
So if your ad gets plenty of clicks but your cover, blurb, reviews, or sample do not
|
||||||
|
convince people to buy, you still pay for the traffic.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In other words, ads do not rescue a weak product page. They simply send more people to it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="campaign-types" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The main campaign types</h2>
|
||||||
|
<p>
|
||||||
|
Amazon offers different types of ad targeting, and each behaves a little differently.
|
||||||
|
The most common options for authors are:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Automatic targeting</strong>, where Amazon decides where to place the ad based on your book</li>
|
||||||
|
<li><strong>Keyword targeting</strong>, where you target search terms readers may use</li>
|
||||||
|
<li><strong>Product targeting</strong>, where you target specific books or similar product pages</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Automatic campaigns can be useful for testing and discovery. Manual campaigns give you more
|
||||||
|
control and often better insight into what is working.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Most authors eventually end up using a mixture rather than relying on one type alone.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="keywords-and-targeting" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Keywords, product targeting and relevance</h2>
|
||||||
|
<p>
|
||||||
|
Advertising works best when the ad is shown to people who are already likely to be interested
|
||||||
|
in your sort of book. That sounds obvious, yet plenty of campaigns get built on vague hopeful
|
||||||
|
thinking rather than actual relevance.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Good targeting often means:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>relevant search keywords</li>
|
||||||
|
<li>comparable books in similar genres or niches</li>
|
||||||
|
<li>authors with overlapping readerships</li>
|
||||||
|
<li>tightly focused themes rather than broad wishful nonsense</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
A thriller should not be wandering drunkenly into places where cosy romance readers are minding
|
||||||
|
their own business, unless you enjoy paying for useless clicks.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="bids-and-budgets" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Bids and daily budgets</h2>
|
||||||
|
<p>
|
||||||
|
Two of the main controls in Amazon Ads are your bid and your daily budget.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Your <strong>bid</strong> is roughly what you are willing to pay for a click. Your
|
||||||
|
<strong>daily budget</strong> is how much you are prepared to spend in a day before the
|
||||||
|
campaign pauses.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
These settings sound simple, but they interact with competition, ad placement and conversion
|
||||||
|
rate. Bid too low and the ad barely appears. Bid too high and you can spend money at a truly
|
||||||
|
irritating speed.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Advertising is basically a controlled experiment in buying attention, so small adjustments are
|
||||||
|
usually wiser than dramatic flailing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="why-one-book-struggles" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why one book often does not make ads profitable</h2>
|
||||||
|
<p>
|
||||||
|
This is the bit a lot of people learn the hard way. If you only have one book, the economics
|
||||||
|
of advertising can be rough.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Your ad spend is competing against the royalty from a single sale. Depending on your format,
|
||||||
|
price point, print cost, and click costs, it can be very difficult to make the numbers work.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That is why many indie authors say the real value of ads becomes clearer once you have a
|
||||||
|
<strong>catalogue</strong>. If a reader discovers one book and then goes on to buy others,
|
||||||
|
the advertising spend starts to support a bigger ecosystem rather than one lonely product.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
A portfolio changes the maths. One book usually takes the hit by itself.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="the-value-of-a-catalogue" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why a catalogue matters</h2>
|
||||||
|
<p>
|
||||||
|
Advertising becomes more interesting when readers have somewhere else to go after the first book.
|
||||||
|
That could mean:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>other books in a series</li>
|
||||||
|
<li>more books by the same author</li>
|
||||||
|
<li>linked standalone titles</li>
|
||||||
|
<li>different formats such as ebook, print and audio</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
In that situation, an ad is not just trying to recover the profit from one sale. It may be
|
||||||
|
helping to acquire a reader who returns more than once.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
That is a much stronger business model than endlessly trying to bully one book into paying for everything.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-makes-an-ad-convert" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What actually makes the ad convert</h2>
|
||||||
|
<p>
|
||||||
|
The ad itself is only the front door. Once a reader clicks, the product page does the real work.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Things that influence conversion include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>cover design</li>
|
||||||
|
<li>book title and subtitle</li>
|
||||||
|
<li>blurb quality</li>
|
||||||
|
<li>reviews and ratings</li>
|
||||||
|
<li>sample readability</li>
|
||||||
|
<li>price</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
If those things are weak, advertising simply delivers more people to a page that fails to persuade them.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="measuring-results" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Measuring results properly</h2>
|
||||||
|
<p>
|
||||||
|
One of the traps with advertising is obsessing over impressions or clicks without paying enough
|
||||||
|
attention to what matters further down the chain.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Things worth watching include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>click-through rate</li>
|
||||||
|
<li>cost per click</li>
|
||||||
|
<li>spend over time</li>
|
||||||
|
<li>sales attributed to the campaign</li>
|
||||||
|
<li>overall profitability, not just activity</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Busy dashboards can look exciting while quietly draining money. Data needs interpretation, not admiration.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="common-mistakes" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Common mistakes authors make</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>running ads before the book page is strong enough</li>
|
||||||
|
<li>bidding too aggressively too early</li>
|
||||||
|
<li>targeting too broadly</li>
|
||||||
|
<li>expecting immediate profit from one book</li>
|
||||||
|
<li>focusing on clicks instead of sales and read-through</li>
|
||||||
|
<li>changing too many variables at once</li>
|
||||||
|
<li>assuming more spend automatically means more success</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-amazon-ads-get-right" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What Amazon Ads get right</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>they place books in front of readers already browsing Amazon</li>
|
||||||
|
<li>they are accessible to indie authors without huge budgets</li>
|
||||||
|
<li>they offer multiple targeting methods</li>
|
||||||
|
<li>they provide measurable campaign data</li>
|
||||||
|
<li>they can support long-term visibility if used sensibly</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-amazon-ads-get-wrong" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What Amazon Ads get wrong</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>they make advertising look easier than it is</li>
|
||||||
|
<li>they encourage spending before authors understand the economics</li>
|
||||||
|
<li>reporting can look clearer than the true business picture</li>
|
||||||
|
<li>they do not solve weak conversion problems</li>
|
||||||
|
<li>they can punish optimism with brutal efficiency</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-amazon-ads" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on Amazon advertising</h2>
|
||||||
|
<p>
|
||||||
|
I think Amazon Ads can be useful, but they need to be approached with realistic expectations.
|
||||||
|
They are not a magic machine for turning books into profit. They are one tool among many for
|
||||||
|
increasing visibility.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The biggest mindset shift is understanding that advertising often makes more sense as part of a
|
||||||
|
longer-term author strategy rather than as a quick win on one title. If readers enjoy one book
|
||||||
|
and go on to buy others, the spend starts to make more sense. Without that wider ecosystem, it
|
||||||
|
can be a much tougher slog.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
So yes, use ads if they suit your strategy... but keep one eye on the numbers and the other on
|
||||||
|
your remaining pound coins.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#what-amazon-ads-are" class="text-decoration-none">What Amazon Ads are</a></li>
|
||||||
|
<li class="mb-2"><a href="#pay-per-click" class="text-decoration-none">Pay per click</a></li>
|
||||||
|
<li class="mb-2"><a href="#campaign-types" class="text-decoration-none">Campaign types</a></li>
|
||||||
|
<li class="mb-2"><a href="#keywords-and-targeting" class="text-decoration-none">Keywords and targeting</a></li>
|
||||||
|
<li class="mb-2"><a href="#bids-and-budgets" class="text-decoration-none">Bids and budgets</a></li>
|
||||||
|
<li class="mb-2"><a href="#why-one-book-struggles" class="text-decoration-none">Why one book struggles</a></li>
|
||||||
|
<li class="mb-2"><a href="#the-value-of-a-catalogue" class="text-decoration-none">Why a catalogue matters</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-makes-an-ad-convert" class="text-decoration-none">What makes an ad convert</a></li>
|
||||||
|
<li class="mb-2"><a href="#measuring-results" class="text-decoration-none">Measuring results</a></li>
|
||||||
|
<li class="mb-2"><a href="#common-mistakes" class="text-decoration-none">Common mistakes</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-amazon-ads-get-right" class="text-decoration-none">What Amazon Ads get right</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-amazon-ads-get-wrong" class="text-decoration-none">What Amazon Ads get wrong</a></li>
|
||||||
|
<li><a href="#my-view-on-amazon-ads" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Amazon Ads can help with visibility, but one book alone often struggles to make the
|
||||||
|
economics work. A catalogue gives advertising far more room to breathe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="text-decoration-none">
|
||||||
|
Reviews and ARC Readers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="text-decoration-none">
|
||||||
|
Author Finances
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp" class="text-decoration-none">
|
||||||
|
Publishing on KDP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="btn btn-dark">
|
||||||
|
Previous: Reviews and ARC Readers
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="btn btn-dark">
|
||||||
|
Next: Author Finances
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
375
CatherineLynwood/Views/IndieAuthor/Audiobooks.cshtml
Normal file
375
CatherineLynwood/Views/IndieAuthor/Audiobooks.cshtml
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Audiobooks and Amazon ACX";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Audiobooks and ACX</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Audiobooks, narration and Amazon ACX</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
Audiobooks can make a book feel gloriously alive. A good narrator adds warmth, tone, character and rhythm
|
||||||
|
in a way the printed page simply cannot. They also introduce a whole new production process, with fresh
|
||||||
|
costs, technical requirements, and plenty of opportunities for things to go slightly sideways.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page covers how indie authors approach audiobook creation, how ACX fits into the picture, how to
|
||||||
|
find a narrator, what editing involves, and why the technical side, chapter files, sound levels, opening
|
||||||
|
credits and all, matters more than many people realise at the start.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="why-audiobooks-matter" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why audiobooks matter</h2>
|
||||||
|
<p>
|
||||||
|
Audiobooks are not just an optional extra any more. For many readers, or rather listeners, they
|
||||||
|
are a major part of how books are consumed. Some people barely sit down with a paperback at all.
|
||||||
|
They listen while driving, walking, working, cooking, cleaning, or trying to avoid hearing their
|
||||||
|
own thoughts for five consecutive minutes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For an indie author, an audiobook can expand the reach of a title and give readers another way
|
||||||
|
into the story. It can also make the work feel more premium and more complete, especially if you
|
||||||
|
are building a proper catalogue rather than just tossing one lonely book into the void and hoping
|
||||||
|
for the best.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The catch is that audio is not just a file conversion job. It is a performance, a production and
|
||||||
|
a technical delivery process all rolled into one.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-is-acx" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What is ACX?</h2>
|
||||||
|
<p>
|
||||||
|
<strong>ACX</strong> is Amazon’s audiobook production and distribution platform. It connects rights
|
||||||
|
holders, such as authors and publishers, with narrators and producers, and provides a route into
|
||||||
|
the Audible and Amazon ecosystem.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For many indie authors, ACX is the most obvious place to start because it offers both the production
|
||||||
|
marketplace and the distribution link in one place. You can use it to find a narrator, agree terms,
|
||||||
|
manage the production process, review the files, and publish the finished audiobook.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In practical terms, it is often the audiobook equivalent of KDP, though with more moving parts and
|
||||||
|
far more dependence on the human performance side of the equation.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="how-the-process-works" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">How the audiobook process works</h2>
|
||||||
|
<p>
|
||||||
|
From the outside, people often imagine audiobooks are made by handing a manuscript to a narrator
|
||||||
|
and waiting for magic to occur. In reality, there is usually a proper workflow.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A typical audiobook process includes:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>preparing the final manuscript</li>
|
||||||
|
<li>choosing whether to narrate it yourself or hire a narrator</li>
|
||||||
|
<li>auditioning and selecting a voice artist</li>
|
||||||
|
<li>agreeing the payment model or royalty arrangement</li>
|
||||||
|
<li>recording the chapters</li>
|
||||||
|
<li>editing and cleaning the audio</li>
|
||||||
|
<li>checking sound levels and technical compliance</li>
|
||||||
|
<li>reviewing proof files</li>
|
||||||
|
<li>approving the final audiobook for distribution</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
None of that is impossible, but it is more involved than uploading a Kindle file and calling it a day.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="finding-a-narrator" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Finding the right narrator</h2>
|
||||||
|
<p>
|
||||||
|
This is one of the biggest creative decisions in the whole process. A narrator can elevate a book
|
||||||
|
beautifully, or drain the life out of it with all the emotional charm of a satnav reading a tax return.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The right narrator is not just somebody with a pleasant voice. They need the right tone for the book,
|
||||||
|
the right pacing, a good ear for character, and the ability to deliver material naturally over many
|
||||||
|
hours without sounding forced or theatrical in the wrong way.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Things worth listening for in auditions include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>clarity and consistency</li>
|
||||||
|
<li>emotional fit for the story</li>
|
||||||
|
<li>accent suitability where relevant</li>
|
||||||
|
<li>handling of dialogue and character voices</li>
|
||||||
|
<li>whether the reading sounds alive rather than merely correct</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
If the narrator does not feel right, the audiobook does not feel right. Simple as that.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="paying-for-production" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Paying for production, royalty share or upfront cost</h2>
|
||||||
|
<p>
|
||||||
|
One of the first financial choices in audiobook production is how the narrator or producer will be
|
||||||
|
paid. Broadly speaking, you are usually looking at either an upfront payment model, a royalty share
|
||||||
|
model, or in some cases a mixture of the two.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
With an upfront model, you pay for the finished audio production directly. That gives you more clarity
|
||||||
|
over cost and generally more control, but it obviously means paying real money before the audiobook has
|
||||||
|
earned a penny.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
With a royalty share arrangement, the narrator shares in the audiobook income instead. That can reduce
|
||||||
|
upfront cost, which is appealing, but it also means you are sharing revenue later and may have fewer
|
||||||
|
suitable narrators willing to work on that basis.
|
||||||
|
</p>
|
||||||
|
<div class="card border-0 bg-light rounded-4 mt-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="h5 fw-bold mb-3">Things to weigh up</h3>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>your available budget</li>
|
||||||
|
<li>the expected market for the audiobook</li>
|
||||||
|
<li>whether you want long-term control over income</li>
|
||||||
|
<li>how attractive your project is to narrators on a royalty basis</li>
|
||||||
|
<li>how quickly you want to move</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="editing-the-audio" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Editing the audio</h2>
|
||||||
|
<p>
|
||||||
|
Audio editing is where a lot of the invisible work lives. Even if the narration performance is strong,
|
||||||
|
the files may still need tidying, noise reduction, spacing adjustments, pickup edits, level balancing,
|
||||||
|
and general polishing so the end result sounds smooth and professional.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This part tends to be wildly underestimated by people who have never done it before. It is not just
|
||||||
|
about trimming a few pauses. It is about making the listening experience feel seamless.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
If you are handling any of this yourself, decent tools and careful listening matter. Audio is ruthless.
|
||||||
|
Once you notice a click, volume jump or weird mouth noise, you cannot unhear the damned thing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="sound-levels-and-technical-requirements" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Sound levels and technical requirements</h2>
|
||||||
|
<p>
|
||||||
|
Audiobook platforms do not just want a nice reading. They also want files that meet technical standards.
|
||||||
|
That means paying attention to things like loudness, peak levels, noise floor, file format and consistent
|
||||||
|
chapter delivery.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is the bit that often catches people out. You can have a great narration and still fail technical
|
||||||
|
checks if the files are not prepared properly.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Typical areas that matter include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>overall loudness level</li>
|
||||||
|
<li>peak level limits</li>
|
||||||
|
<li>background noise or hiss</li>
|
||||||
|
<li>consistent sound from chapter to chapter</li>
|
||||||
|
<li>clean opening and closing space</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Glamorous? No. Important? Absolutely.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="chapter-files-and-credits" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Chapter files, opening credits and closing credits</h2>
|
||||||
|
<p>
|
||||||
|
Audiobooks are normally delivered as separate audio files for each chapter or section, along with opening
|
||||||
|
and closing credits. The structure matters because it affects both the listener experience and platform
|
||||||
|
acceptance.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You usually need to think about:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>intro and title announcements</li>
|
||||||
|
<li>chapter naming and ordering</li>
|
||||||
|
<li>front matter and back matter</li>
|
||||||
|
<li>end credits wording</li>
|
||||||
|
<li>clean file naming and organisation</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
A tidy structure makes everything easier, from proofing to upload to later corrections.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="proofing-and-approvals" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Proofing and approvals</h2>
|
||||||
|
<p>
|
||||||
|
Before the audiobook goes live, somebody needs to listen through and catch mistakes. That can include
|
||||||
|
misreads, repeated lines, missing words, awkward pronunciations, pacing problems, technical faults, or
|
||||||
|
anything else that jars.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Proofing audio is time-consuming because there is no shortcut around actually listening. If the audiobook
|
||||||
|
is ten hours long, there are not many magical ways around spending a large amount of time with it.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This is one of those stages where patience pays off. Rushing here is how errors make it into the final release.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="cover-and-metadata" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Audiobook cover and metadata</h2>
|
||||||
|
<p>
|
||||||
|
The audiobook also needs proper presentation. That includes suitable cover artwork in the right format,
|
||||||
|
clean metadata, author and narrator details, title information, and a professional product description.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Even though the listener experiences the book through sound, the cover still matters. People are still
|
||||||
|
browsing store pages, still making snap judgments, and still deciding whether the book looks worth their time.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Audio may be heard with the ears, but it is still sold with the packaging.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-acx-gets-right" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What ACX gets right</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>It connects authors with narrators and producers in one place</li>
|
||||||
|
<li>It gives indie authors a realistic route into the Audible ecosystem</li>
|
||||||
|
<li>It provides a defined production workflow</li>
|
||||||
|
<li>It helps structure the approval and delivery process</li>
|
||||||
|
<li>It lowers the barrier to entering audiobook publishing</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-acx-gets-wrong" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What ACX gets wrong</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>It can make audiobook production look simpler than it really is</li>
|
||||||
|
<li>The technical side can feel intimidating for new authors</li>
|
||||||
|
<li>Distribution and exclusivity choices can be restrictive</li>
|
||||||
|
<li>It still depends heavily on finding the right narrator, which is never a guaranteed quick win</li>
|
||||||
|
<li>Audio production is slow compared with ebook publishing</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-audiobooks" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on audiobooks as an indie author</h2>
|
||||||
|
<p>
|
||||||
|
Audiobooks are brilliant, but they are not the easy add-on some people imagine. They take time, attention,
|
||||||
|
money, technical care and a lot of listening. When done well, though, they can add enormous value to a book
|
||||||
|
and create a completely different experience for readers.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I think the key is to treat the audiobook as its own production, not as an afterthought. It deserves the same
|
||||||
|
care as the print and ebook versions, because once listeners press play, they are trusting you with hours of
|
||||||
|
their time and a voice will now carry your story instead of the reader’s own imagination.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
That is a powerful thing when it works. And painfully obvious when it does not.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#why-audiobooks-matter" class="text-decoration-none">Why audiobooks matter</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-is-acx" class="text-decoration-none">What is ACX?</a></li>
|
||||||
|
<li class="mb-2"><a href="#how-the-process-works" class="text-decoration-none">How the process works</a></li>
|
||||||
|
<li class="mb-2"><a href="#finding-a-narrator" class="text-decoration-none">Finding a narrator</a></li>
|
||||||
|
<li class="mb-2"><a href="#paying-for-production" class="text-decoration-none">Paying for production</a></li>
|
||||||
|
<li class="mb-2"><a href="#editing-the-audio" class="text-decoration-none">Editing the audio</a></li>
|
||||||
|
<li class="mb-2"><a href="#sound-levels-and-technical-requirements" class="text-decoration-none">Sound levels and requirements</a></li>
|
||||||
|
<li class="mb-2"><a href="#chapter-files-and-credits" class="text-decoration-none">Chapter files and credits</a></li>
|
||||||
|
<li class="mb-2"><a href="#proofing-and-approvals" class="text-decoration-none">Proofing and approvals</a></li>
|
||||||
|
<li class="mb-2"><a href="#cover-and-metadata" class="text-decoration-none">Cover and metadata</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-acx-gets-right" class="text-decoration-none">What ACX gets right</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-acx-gets-wrong" class="text-decoration-none">What ACX gets wrong</a></li>
|
||||||
|
<li><a href="#my-view-on-audiobooks" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Audiobooks can hugely expand a book’s reach, but they are a full production job, not a quick export button.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp" class="text-decoration-none">
|
||||||
|
Publishing on KDP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
|
||||||
|
Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="text-decoration-none">
|
||||||
|
Author Finances
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ingramSpark" class="btn btn-dark">
|
||||||
|
Previous: Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="btn btn-dark">
|
||||||
|
Next: Reviews and ARC Readers
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
394
CatherineLynwood/Views/IndieAuthor/AuthorFinances.cshtml
Normal file
394
CatherineLynwood/Views/IndieAuthor/AuthorFinances.cshtml
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Author Finances";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Author Finances</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Author finances and the real cost of self-publishing</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
One of the biggest myths around self-publishing is that once your book is live, the money simply
|
||||||
|
starts rolling in. It does not. Or at least, not for most people. The financial side of indie
|
||||||
|
publishing is a mixture of costs, royalties, advertising spend, platform deductions, print
|
||||||
|
margins, and the occasional unpleasant surprise.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page looks at the financial reality behind independent publishing, including setup costs,
|
||||||
|
royalties, print economics, advertising, and why building a catalogue usually matters far more
|
||||||
|
than expecting a single book to carry the whole operation.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="the-basic-financial-picture" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The basic financial picture</h2>
|
||||||
|
<p>
|
||||||
|
Self-publishing income is rarely a simple case of retail price minus profit. Every format
|
||||||
|
comes with its own pricing model, platform deductions, and practical limitations.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Depending on how you publish, the money coming in may be reduced by:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>printing costs</li>
|
||||||
|
<li>platform commission</li>
|
||||||
|
<li>wholesale discounts</li>
|
||||||
|
<li>advertising spend</li>
|
||||||
|
<li>production costs such as editing, cover design or audio</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
It is perfectly possible to sell books and still make very little actual profit, especially early on.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="upfront-costs" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Upfront costs</h2>
|
||||||
|
<p>
|
||||||
|
Before a book earns anything, it usually costs something. The exact amount varies wildly depending
|
||||||
|
on how much you do yourself and how much you outsource.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Typical cost area</th>
|
||||||
|
<th>Examples</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Editing</td>
|
||||||
|
<td>Professional editing, proofreading, beta reading support</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Cover design</td>
|
||||||
|
<td>Designer fees, stock assets, software, image generation tools</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Formatting</td>
|
||||||
|
<td>Interior layout tools, formatting services, proof copies</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ISBNs</td>
|
||||||
|
<td>Purchasing and registering your own ISBNs</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Audio production</td>
|
||||||
|
<td>Narration, editing, mastering, platform preparation</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Marketing</td>
|
||||||
|
<td>Amazon Ads, promotional graphics, giveaways, website costs</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
Some authors keep these costs low by doing a lot themselves. Others invest heavily upfront.
|
||||||
|
Neither approach guarantees success. It simply changes the risk profile.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="royalties-and-income-streams" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Royalties and income streams</h2>
|
||||||
|
<p>
|
||||||
|
Self-published authors often earn income from several different sources rather than one neat pot.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Common income streams include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Kindle ebook sales</li>
|
||||||
|
<li>Kindle Unlimited page reads</li>
|
||||||
|
<li>paperback sales</li>
|
||||||
|
<li>hardback sales</li>
|
||||||
|
<li>audiobook sales</li>
|
||||||
|
<li>direct sales, if you offer them</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Each of these may have very different margins, reporting delays, and patterns of performance.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ebook-economics" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Ebook economics</h2>
|
||||||
|
<p>
|
||||||
|
Ebooks are often the cleanest financial model in self-publishing because there are no print costs.
|
||||||
|
That does not mean every sale is highly profitable, but it does mean the margin is usually easier
|
||||||
|
to understand than with print.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Pricing still matters, though. Ebook royalties are shaped by the platform rules and the price band
|
||||||
|
you choose. Price too low and you may struggle to earn enough. Price too high and readers may simply
|
||||||
|
walk away.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Ebook pricing is not just about what feels fair to the author. It is about what the market will accept.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="print-economics" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Print economics</h2>
|
||||||
|
<p>
|
||||||
|
Print books are where many authors meet the cold wind of reality. Long books cost more to print,
|
||||||
|
hardbacks cost more than paperbacks, and distribution discounts can eat into margins very quickly.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For print books, the basic financial picture usually involves:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>retail price</li>
|
||||||
|
<li>printing cost</li>
|
||||||
|
<li>platform share or wholesale discount</li>
|
||||||
|
<li>author margin</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
That is why a beautifully chunky hardback can feel emotionally satisfying while being financially
|
||||||
|
far less exciting than outsiders imagine.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ingram-and-wholesale" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">IngramSpark, wholesale discounts and margins</h2>
|
||||||
|
<p>
|
||||||
|
IngramSpark introduces a different kind of financial thinking because it is built around access
|
||||||
|
to the wider book trade. That means wholesale discount becomes a key part of the model.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Retailers need room to make their own margin, which means the publisher, in this case you,
|
||||||
|
gives up part of the cover price before printing costs are even taken into account.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The result is that a book can look expensive to readers while still generating only a modest return
|
||||||
|
for the author.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="returns-risk" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Returns risk</h2>
|
||||||
|
<p>
|
||||||
|
If you enable returns through IngramSpark, you are stepping into one of the weirder traditions of
|
||||||
|
the book trade. Retailers can return unsold books, and the financial consequences come back to you.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That means the wrong settings can create situations where distribution looks promising on paper but
|
||||||
|
carries genuine financial exposure in practice.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Returns are one of those things every indie author should understand before casually clicking yes.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="advertising-costs" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Advertising costs</h2>
|
||||||
|
<p>
|
||||||
|
Advertising is usually one of the fastest-moving expenses in self-publishing. Unlike cover design
|
||||||
|
or ISBNs, which are relatively fixed, ads can keep draining money every day if you let them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Common realities include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>clicks cost money whether or not readers buy</li>
|
||||||
|
<li>one book often struggles to make PPC profitable</li>
|
||||||
|
<li>catalogue depth can make ad spend more worthwhile</li>
|
||||||
|
<li>poor conversion can quietly waste budget</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Advertising can help visibility, but it is not free momentum. It is paid exposure.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="the-portfolio-effect" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why a portfolio of books changes the maths</h2>
|
||||||
|
<p>
|
||||||
|
A single book has to recover its own costs by itself. That can be a hard road. Once you have multiple
|
||||||
|
books, the picture changes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A reader acquired through one title might go on to:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>buy other books in the series</li>
|
||||||
|
<li>try your backlist</li>
|
||||||
|
<li>pick up other formats such as audio or hardback</li>
|
||||||
|
<li>become a repeat reader over time</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
That is why many indie authors treat the first book less as a profit engine and more as the beginning
|
||||||
|
of a catalogue.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="cash-flow-vs-profit" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Cash flow vs actual profit</h2>
|
||||||
|
<p>
|
||||||
|
Seeing royalty income arrive can feel encouraging, and rightly so, but income is not the same thing
|
||||||
|
as profit. Properly understanding the finances means comparing money coming in with money already spent.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That includes things like:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>software subscriptions</li>
|
||||||
|
<li>proof copies</li>
|
||||||
|
<li>cover and editing costs</li>
|
||||||
|
<li>advertising spend</li>
|
||||||
|
<li>website and hosting costs</li>
|
||||||
|
<li>promotional materials</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
A book can have sales momentum and still be recovering its investment rather than genuinely “making money” yet.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="why-most-first-books-dont-profit" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why most first books do not make much money</h2>
|
||||||
|
<p>
|
||||||
|
This is not pessimism. It is just the boring old truth. Most first books do not generate huge profits.
|
||||||
|
Some barely recover their direct costs at all.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That usually comes down to a few simple reasons:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>the author has not yet built an audience</li>
|
||||||
|
<li>there is no backlist to support read-through</li>
|
||||||
|
<li>marketing is still being learned</li>
|
||||||
|
<li>production costs land before meaningful sales do</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
This is why treating Book One as the foundation of something bigger often makes more sense than expecting
|
||||||
|
immediate riches.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="showing-real-numbers" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Showing real numbers</h2>
|
||||||
|
<p>
|
||||||
|
One of the most useful things an author website can do is show approximate real figures rather than vague
|
||||||
|
motivational waffle. That could include examples such as:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>print cost versus retail price</li>
|
||||||
|
<li>royalty per paperback or hardback sale</li>
|
||||||
|
<li>ebook royalty examples at different price points</li>
|
||||||
|
<li>advertising spend versus attributed sales</li>
|
||||||
|
<li>monthly or lifetime totals by format</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page could eventually become a live balance-sheet style section connected to backend data, which
|
||||||
|
would make it far more useful than the average “you can do it” author advice page.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-author-finances" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on author finances</h2>
|
||||||
|
<p>
|
||||||
|
I think the healthiest way to look at self-publishing finances is with honesty and patience. The first book
|
||||||
|
may not make much money. It may not even break even for a while. That does not automatically mean it has failed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Sometimes the real value of a book is that it establishes your name, builds experience, attracts readers,
|
||||||
|
and becomes part of a growing body of work. Financially, the long game usually matters more than the first
|
||||||
|
little spike of sales.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The dream is not just to publish a book. It is to build something that can keep earning over time.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#the-basic-financial-picture" class="text-decoration-none">The basic financial picture</a></li>
|
||||||
|
<li class="mb-2"><a href="#upfront-costs" class="text-decoration-none">Upfront costs</a></li>
|
||||||
|
<li class="mb-2"><a href="#royalties-and-income-streams" class="text-decoration-none">Royalties and income streams</a></li>
|
||||||
|
<li class="mb-2"><a href="#ebook-economics" class="text-decoration-none">Ebook economics</a></li>
|
||||||
|
<li class="mb-2"><a href="#print-economics" class="text-decoration-none">Print economics</a></li>
|
||||||
|
<li class="mb-2"><a href="#ingram-and-wholesale" class="text-decoration-none">Ingram and wholesale</a></li>
|
||||||
|
<li class="mb-2"><a href="#returns-risk" class="text-decoration-none">Returns risk</a></li>
|
||||||
|
<li class="mb-2"><a href="#advertising-costs" class="text-decoration-none">Advertising costs</a></li>
|
||||||
|
<li class="mb-2"><a href="#the-portfolio-effect" class="text-decoration-none">Portfolio effect</a></li>
|
||||||
|
<li class="mb-2"><a href="#cash-flow-vs-profit" class="text-decoration-none">Cash flow vs profit</a></li>
|
||||||
|
<li class="mb-2"><a href="#why-most-first-books-dont-profit" class="text-decoration-none">Why first books struggle</a></li>
|
||||||
|
<li class="mb-2"><a href="#showing-real-numbers" class="text-decoration-none">Showing real numbers</a></li>
|
||||||
|
<li><a href="#my-view-on-author-finances" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Self-publishing income is real, but so are the costs. The financial picture usually makes far
|
||||||
|
more sense once you have multiple books rather than one title doing all the heavy lifting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="text-decoration-none">
|
||||||
|
Amazon Advertising
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
|
||||||
|
Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
|
||||||
|
KDP vs IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="btn btn-dark">
|
||||||
|
Previous: Amazon Advertising
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="btn btn-dark">
|
||||||
|
Next: AI for Authors
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
356
CatherineLynwood/Views/IndieAuthor/CoverDesign.cshtml
Normal file
356
CatherineLynwood/Views/IndieAuthor/CoverDesign.cshtml
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Cover Design";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Cover Design</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Cover design for indie authors</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
However much writers may wish otherwise, readers absolutely judge a book by its cover.
|
||||||
|
The cover is not just decoration. It is packaging, branding, positioning and salesmanship
|
||||||
|
all rolled into one. Before anyone reads the blurb, downloads a sample or listens to a chapter,
|
||||||
|
the cover is already doing its work, or failing to.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page looks at what makes a strong cover, the realities of designing one as an indie author,
|
||||||
|
how AI can be useful as part of the creative process, and why getting the cover good enough to
|
||||||
|
compete matters far more than indulging every artistic whim you happen to have that week.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="why-covers-matter" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why covers matter so much</h2>
|
||||||
|
<p>
|
||||||
|
A cover has one brutal job, to make the right reader stop scrolling.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It needs to signal genre, tone and quality within seconds. A good cover helps a reader
|
||||||
|
feel that the book belongs in the same world as other books they already enjoy. A weak
|
||||||
|
cover creates hesitation, and hesitation kills clicks.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The painful truth is that even a brilliant story can be quietly sabotaged by a poor cover.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-a-cover-needs-to-do" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What a cover actually needs to do</h2>
|
||||||
|
<p>
|
||||||
|
A strong cover is not just about looking pretty. It needs to work as a piece of visual communication.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
At minimum, a cover should:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>look professional</li>
|
||||||
|
<li>match the genre or market expectation</li>
|
||||||
|
<li>remain clear at thumbnail size</li>
|
||||||
|
<li>present the title and author name clearly</li>
|
||||||
|
<li>create curiosity or emotional pull</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
If it fails at those things, it does not matter how meaningful the symbolism is to you personally.
|
||||||
|
Readers are not standing in a gallery decoding your artistic soul. They are shopping.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="genre-signals" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Genre signals and reader expectations</h2>
|
||||||
|
<p>
|
||||||
|
Covers work partly because readers learn visual shorthand. Certain fonts, colour palettes,
|
||||||
|
layouts and image styles immediately suggest thriller, romance, fantasy, historical fiction,
|
||||||
|
literary fiction, and so on.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That does not mean every cover must be identical, but it does mean a cover should not confuse
|
||||||
|
the target audience. If your thriller looks like light women’s fiction, or your dark family
|
||||||
|
drama looks like a cosy village mystery, you are making life harder for yourself.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
A cover should promise the right sort of reading experience before the reader even clicks.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="thumbnail-test" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The thumbnail test</h2>
|
||||||
|
<p>
|
||||||
|
One of the most important tests for any modern cover is how it looks at a very small size.
|
||||||
|
Most readers first encounter books as tiny rectangles on a screen, not as glorious full-size
|
||||||
|
jackets under warm bookshop lighting. In many cases these thumbnails are also in black and white.
|
||||||
|
Only recently have Amazon started to produce Colour Kindle. My very first cover looked great,
|
||||||
|
right up to the point where you viewed it as black and white. At that point it merged into a
|
||||||
|
mess of grey and greyer. Not pretty, and no use on a Kindle search.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
At thumbnail size, the cover should still:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>be readable enough to identify</li>
|
||||||
|
<li>have a strong overall silhouette</li>
|
||||||
|
<li>avoid looking muddy or cluttered</li>
|
||||||
|
<li>hold together visually</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
If the design only works when viewed full screen at 200 percent, it is not really working where it matters.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="title-and-typography" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Title treatment and typography</h2>
|
||||||
|
<p>
|
||||||
|
Typography does a huge amount of heavy lifting on a cover. The font choice, size, spacing
|
||||||
|
and position of the title can change the whole feel of the design.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Common mistakes include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>fonts that do not suit the genre</li>
|
||||||
|
<li>text that is too small</li>
|
||||||
|
<li>poor contrast against the background</li>
|
||||||
|
<li>too many different font styles fighting each other</li>
|
||||||
|
<li>trying to be clever when simple would be stronger</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
If readers cannot quickly read the title, that is not mysterious. It is just annoying.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="doing-it-yourself" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Doing it yourself as an indie author</h2>
|
||||||
|
<p>
|
||||||
|
Many indie authors design their own covers, at least initially, either to save money or because
|
||||||
|
they already have some design skills. That can work very well, but it comes with a trap.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The trap is that authors are often too emotionally attached to the story to see the cover as a sales tool.
|
||||||
|
They start designing for themselves rather than for the reader.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
If you design your own cover, you need to think like a marketer as well as a creator.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="working-with-a-designer" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Working with a designer</h2>
|
||||||
|
<p>
|
||||||
|
Hiring a professional cover designer can be a very smart investment, especially if design is not
|
||||||
|
your strength or you want a result that competes immediately at a high level.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A good designer can help with:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>genre positioning</li>
|
||||||
|
<li>clean typography</li>
|
||||||
|
<li>layout and hierarchy</li>
|
||||||
|
<li>print-ready files</li>
|
||||||
|
<li>consistency across a series or author brand</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
The trick is finding someone who understands the market for your type of book, not just someone
|
||||||
|
who can make attractive pictures.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="using-ai-for-inspiration" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Using AI for inspiration</h2>
|
||||||
|
<p>
|
||||||
|
AI can be genuinely useful in the cover process, not necessarily as the final cover itself,
|
||||||
|
but as a way of exploring mood, composition, character looks, setting ideas and visual possibilities.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It can help authors:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>test visual ideas quickly</li>
|
||||||
|
<li>clarify what they are trying to describe</li>
|
||||||
|
<li>explore colour and atmosphere</li>
|
||||||
|
<li>create reference material for later design work</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Used sensibly, AI is not replacing the cover design process. It is helping spark and refine ideas.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="ai-strengths-and-limits" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The strengths and limits of AI-generated imagery</h2>
|
||||||
|
<p>
|
||||||
|
AI image tools can be brilliant for concept exploration, but they are not magical.
|
||||||
|
They can produce striking visuals quickly, but they can also generate bizarre details,
|
||||||
|
inconsistent anatomy, muddled typography, and compositions that collapse the moment
|
||||||
|
you ask for something specific and technically precise.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This makes them useful for inspiration, mockups and experimentation, but not automatically
|
||||||
|
a replacement for proper design judgement.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
A cover still needs human taste, selection and refinement, otherwise you are just letting the machine
|
||||||
|
throw visual spaghetti at the wall and hoping some of it looks expensive.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="print-vs-ebook-cover" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Ebook covers vs full print covers</h2>
|
||||||
|
<p>
|
||||||
|
It is worth remembering that an ebook cover and a full print cover are not the same thing.
|
||||||
|
The front cover is only part of the package for print editions.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Print preparation may also involve:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>spine width calculation</li>
|
||||||
|
<li>back cover layout</li>
|
||||||
|
<li>barcode placement</li>
|
||||||
|
<li>bleed and trim setup</li>
|
||||||
|
<li>colour profile considerations</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
A design that looks fine as a flat front cover still needs to survive the practical realities of print production.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="common-cover-mistakes" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Common cover mistakes</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>trying to cram too much onto the cover</li>
|
||||||
|
<li>poor text contrast</li>
|
||||||
|
<li>weak hierarchy between title and author name</li>
|
||||||
|
<li>using imagery that does not match the book’s tone</li>
|
||||||
|
<li>designing for personal symbolism instead of market impact</li>
|
||||||
|
<li>forgetting how the cover looks at thumbnail size</li>
|
||||||
|
<li>letting AI output dictate the design without proper editing</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="series-branding" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Series branding and consistency</h2>
|
||||||
|
<p>
|
||||||
|
If you are writing a series, cover consistency becomes even more important. Readers should be able
|
||||||
|
to recognise related books at a glance.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That does not mean every cover must be identical, but it should feel as though the books belong together.
|
||||||
|
Consistency in fonts, layout, colour treatment or visual style helps create a recognisable brand.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
That is especially valuable online, where your books may appear next to one another on a retailer page.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-cover-design" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on cover design</h2>
|
||||||
|
<p>
|
||||||
|
I think cover design is one of the areas where indie authors have to be especially honest with themselves.
|
||||||
|
Writing a book does not automatically make someone a strong cover designer, in the same way owning a frying pan
|
||||||
|
does not automatically make someone a chef.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That does not mean authors should not be involved. Quite the opposite. But the goal should be to create a cover
|
||||||
|
that works in the market, not simply one that feels emotionally satisfying in isolation.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The best cover is not the one that explains everything. It is the one that makes the right reader want to know more.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#why-covers-matter" class="text-decoration-none">Why covers matter</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-a-cover-needs-to-do" class="text-decoration-none">What a cover needs to do</a></li>
|
||||||
|
<li class="mb-2"><a href="#genre-signals" class="text-decoration-none">Genre signals</a></li>
|
||||||
|
<li class="mb-2"><a href="#thumbnail-test" class="text-decoration-none">The thumbnail test</a></li>
|
||||||
|
<li class="mb-2"><a href="#title-and-typography" class="text-decoration-none">Title and typography</a></li>
|
||||||
|
<li class="mb-2"><a href="#doing-it-yourself" class="text-decoration-none">Doing it yourself</a></li>
|
||||||
|
<li class="mb-2"><a href="#working-with-a-designer" class="text-decoration-none">Working with a designer</a></li>
|
||||||
|
<li class="mb-2"><a href="#using-ai-for-inspiration" class="text-decoration-none">Using AI for inspiration</a></li>
|
||||||
|
<li class="mb-2"><a href="#ai-strengths-and-limits" class="text-decoration-none">AI strengths and limits</a></li>
|
||||||
|
<li class="mb-2"><a href="#print-vs-ebook-cover" class="text-decoration-none">Ebook vs print cover</a></li>
|
||||||
|
<li class="mb-2"><a href="#common-cover-mistakes" class="text-decoration-none">Common mistakes</a></li>
|
||||||
|
<li class="mb-2"><a href="#series-branding" class="text-decoration-none">Series branding</a></li>
|
||||||
|
<li><a href="#my-view-on-cover-design" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
A cover is not just art. It is a sales tool. It needs to signal genre, look professional,
|
||||||
|
and work at thumbnail size if it is going to do its job properly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="text-decoration-none">
|
||||||
|
AI for Authors
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="text-decoration-none">
|
||||||
|
Amazon Advertising
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="text-decoration-none">
|
||||||
|
Traditional vs Self Publishing
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="EditingAndProofReading" class="btn btn-dark">
|
||||||
|
Previous: Editing and Proofreading
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="btn btn-dark">
|
||||||
|
Next: KDP vs IngramSpark
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
398
CatherineLynwood/Views/IndieAuthor/EditingAndProofreading.cshtml
Normal file
398
CatherineLynwood/Views/IndieAuthor/EditingAndProofreading.cshtml
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Editing and Proofreading";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Editing and Proofreading</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Editing and proofreading as an indie author</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
Finishing a manuscript is not the same thing as finishing a book. In fact, for most authors,
|
||||||
|
the first full draft is where the real work begins. Editing is where the story gets sharper,
|
||||||
|
cleaner, more emotionally effective, and less likely to contain the same phrase three times
|
||||||
|
in one paragraph like it has developed a nervous twitch.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page looks at the difference between editing and proofreading, the stages involved,
|
||||||
|
the value of reading work aloud, and how tools such as AI voice playback can help expose
|
||||||
|
awkward phrasing, repetition, typos, pacing problems, and the sort of errors your eyes
|
||||||
|
glide straight past because your brain thinks it already knows what the sentence says.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="why-editing-matters" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why editing matters</h2>
|
||||||
|
<p>
|
||||||
|
A strong idea can be weakened by clumsy delivery. A powerful scene can lose impact
|
||||||
|
if it drags, repeats itself, or says too much. A good book is not usually written
|
||||||
|
in one clean burst of genius. It is shaped.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Editing is the stage where you begin turning a raw manuscript into something that
|
||||||
|
feels intentional. It is where you spot structural problems, smooth out the prose,
|
||||||
|
tighten the dialogue, strengthen weak transitions, and remove the bits that only made
|
||||||
|
sense because you, the author, already knew what you meant.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In other words, editing is not a chore stapled awkwardly onto writing. It is a huge
|
||||||
|
part of writing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="editing-vs-proofreading" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Editing vs proofreading</h2>
|
||||||
|
<p>
|
||||||
|
These terms often get lumped together, but they are not the same thing.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Stage</th>
|
||||||
|
<th>Main focus</th>
|
||||||
|
<th>Typical issues</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Editing</td>
|
||||||
|
<td>Improving the writing itself</td>
|
||||||
|
<td>Structure, pacing, repetition, clarity, tone, dialogue, scene order</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Proofreading</td>
|
||||||
|
<td>Correcting final surface errors</td>
|
||||||
|
<td>Typos, spelling, punctuation, formatting slips, missing words</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
Put simply, editing changes the book for the better. Proofreading catches the little
|
||||||
|
gremlins still running around after the heavy lifting is done.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="the-main-stages" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The main stages of editing</h2>
|
||||||
|
<p>
|
||||||
|
Different authors work differently, but most manuscripts benefit from being tackled
|
||||||
|
in layers rather than trying to fix absolutely everything at once.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Typical stages include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Structural editing</strong>, looking at plot, chapter order, pacing, character arcs and overall flow</li>
|
||||||
|
<li><strong>Line editing</strong>, improving the wording, tone, rhythm and readability sentence by sentence</li>
|
||||||
|
<li><strong>Copy editing</strong>, checking grammar, consistency, punctuation and technical accuracy</li>
|
||||||
|
<li><strong>Proofreading</strong>, catching final typos and formatting errors before publication</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Trying to do all of that simultaneously is a lovely way to go half mad and still miss things.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="structural-editing" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Structural editing</h2>
|
||||||
|
<p>
|
||||||
|
Structural editing is the big-picture stage. This is where you look at whether the
|
||||||
|
story itself works.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Questions at this stage might include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Does the chapter order make sense?</li>
|
||||||
|
<li>Does the middle sag?</li>
|
||||||
|
<li>Are emotional beats landing properly?</li>
|
||||||
|
<li>Is there repeated information the reader only needs once?</li>
|
||||||
|
<li>Do character choices feel believable?</li>
|
||||||
|
<li>Is anything missing that the story actually needs?</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
This is not the moment to fuss over commas while an entire subplot is limping along with one leg missing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="line-editing" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Line editing and tightening the prose</h2>
|
||||||
|
<p>
|
||||||
|
Once the structure is sound, the line-by-line work becomes much more worthwhile.
|
||||||
|
This is where you improve the actual reading experience.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Line editing often involves:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>cutting repetition</li>
|
||||||
|
<li>improving rhythm and flow</li>
|
||||||
|
<li>removing clunky phrasing</li>
|
||||||
|
<li>tightening dialogue</li>
|
||||||
|
<li>making description more precise</li>
|
||||||
|
<li>reducing over-explanation</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
This stage is where prose starts sounding less like a draft and more like a finished book.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="proofreading-stage" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Proofreading the final version</h2>
|
||||||
|
<p>
|
||||||
|
Proofreading comes late, once the major editing decisions are done. There is no point
|
||||||
|
polishing sentences you are about to delete, nor lovingly correcting punctuation in a
|
||||||
|
chapter that might get split in two next week.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This final pass is about catching the stubborn leftovers:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>spelling mistakes</li>
|
||||||
|
<li>missing or repeated words</li>
|
||||||
|
<li>punctuation slips</li>
|
||||||
|
<li>inconsistent capitalisation</li>
|
||||||
|
<li>small formatting errors</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Proofreading is less glamorous than structural editing, but it is the stage that stops readers
|
||||||
|
tripping over obvious errors and muttering darkly about standards.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="reading-aloud" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why reading aloud works so well</h2>
|
||||||
|
<p>
|
||||||
|
Reading text aloud, or hearing it read aloud, is one of the most effective editing tools
|
||||||
|
there is. The ear catches things the eye happily skips.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A sentence can look fine on the page and still sound awkward once spoken. Repetition becomes
|
||||||
|
more obvious. Dialogue reveals whether it sounds natural or stilted. Overlong sentences stop
|
||||||
|
pretending to be elegant and instead collapse wheezing into the furniture.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
If something sounds wrong, it often is wrong, even if you cannot immediately explain why.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="using-ai-voices" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Using AI voices to catch mistakes</h2>
|
||||||
|
<p>
|
||||||
|
One of the most useful modern editing methods is to use AI-generated voice playback
|
||||||
|
to listen back to your chapters. It creates enough distance from the text that you begin
|
||||||
|
hearing the words more like a reader would experience them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This can be brilliant for spotting:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>repeated phrases</li>
|
||||||
|
<li>typos that the eye keeps missing</li>
|
||||||
|
<li>unnatural dialogue</li>
|
||||||
|
<li>rhythm problems</li>
|
||||||
|
<li>accidental word duplication</li>
|
||||||
|
<li>sentences that are technically correct but still sound wrong</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
It is especially useful because your brain is far too willing to autocorrect your own work
|
||||||
|
as you read it silently. Hearing the text externally cuts through that nonsense.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I use a service called ElevenReader. It's by those clever ElevenLabs people. Now everyone has differing
|
||||||
|
opinions about using AI, but hear me out... literally.
|
||||||
|
</p>
|
||||||
|
<audio controls class="w-100">
|
||||||
|
<source src="~/audio/editing-and-proof-reading.mp3" type="audio/mpeg" />
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
<p class="small">
|
||||||
|
Read on or click the play button.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When I write a chapter, and especially if it's a longer one I load it up into the ElevenReader app
|
||||||
|
and listen to it back many times. It's like having a friend or family member read your work back to
|
||||||
|
you, only they don't get fed up the 10th time you ask them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What I tend to do is listen to it with the manuscript in front of me. You'd be surprised at how many
|
||||||
|
typos and misused words you include and would not see if you were purely editing the document alone.
|
||||||
|
One of the mistakes I made in my first book was typing the word "form" instead of "from". I must have
|
||||||
|
read that passage twenty times and never saw it, but as soon as I listened to it back, it stood out like
|
||||||
|
a sore thumb.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Repetition is another gotcha. I found I would use words like "amazing" far too often. I'd write them in
|
||||||
|
dialogue and again, on the page it looked fine. But, oh my word, when you hear it back, you realise how
|
||||||
|
awful it sounds. Seriously... try it. You'll thank me.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Used properly, AI voice playback is not replacing editing. It is giving you another angle of attack.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="common-things-to-watch-for" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Common things to watch for</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>repetition of words, ideas or emotional beats</li>
|
||||||
|
<li>characters saying the same thing in slightly different ways</li>
|
||||||
|
<li>scenes that start too early or end too late</li>
|
||||||
|
<li>awkward transitions between chapters</li>
|
||||||
|
<li>overwritten description</li>
|
||||||
|
<li>dialogue that sounds written rather than spoken</li>
|
||||||
|
<li>small continuity slips</li>
|
||||||
|
<li>paragraphs that simply go on too long</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="beta-readers-and-feedback" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Beta readers and outside feedback</h2>
|
||||||
|
<p>
|
||||||
|
However carefully you edit, there comes a point where you are too close to the work.
|
||||||
|
Fresh eyes matter.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Beta readers can help identify:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>confusing sections</li>
|
||||||
|
<li>slow chapters</li>
|
||||||
|
<li>unconvincing character decisions</li>
|
||||||
|
<li>plot points that need clearer setup</li>
|
||||||
|
<li>moments that are emotionally stronger or weaker than you realised</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Not every piece of feedback should be obeyed like holy scripture, but patterns matter. If several people
|
||||||
|
stumble over the same section, the section is probably the problem.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="editing-over-time" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why time helps</h2>
|
||||||
|
<p>
|
||||||
|
Distance improves editing. Leaving a manuscript alone for a while, even briefly, can make a huge difference.
|
||||||
|
When you come back, you are more likely to notice what is actually on the page rather than what you remember writing.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is one reason long projects often improve through multiple passes spread over time. The more distance you gain,
|
||||||
|
the less your brain protects the text from criticism.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Annoying, really, because it means patience helps and everyone wants the book finished yesterday.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-editing" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on editing as an indie author</h2>
|
||||||
|
<p>
|
||||||
|
Editing is where a book earns its right to exist in public. Drafting creates the raw material,
|
||||||
|
but editing is where that material becomes readable, controlled and deliberate.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I think one of the most useful things an indie author can do is build a repeatable editing process.
|
||||||
|
That might include silent reading, printed markup, AI voice playback, beta readers, and multiple passes
|
||||||
|
focused on different problems. The exact method can vary, but the principle is the same... do not rely on one pass.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Your first draft is you telling yourself the story. Editing is you making it work for everyone else.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#why-editing-matters" class="text-decoration-none">Why editing matters</a></li>
|
||||||
|
<li class="mb-2"><a href="#editing-vs-proofreading" class="text-decoration-none">Editing vs proofreading</a></li>
|
||||||
|
<li class="mb-2"><a href="#the-main-stages" class="text-decoration-none">The main stages</a></li>
|
||||||
|
<li class="mb-2"><a href="#structural-editing" class="text-decoration-none">Structural editing</a></li>
|
||||||
|
<li class="mb-2"><a href="#line-editing" class="text-decoration-none">Line editing</a></li>
|
||||||
|
<li class="mb-2"><a href="#proofreading-stage" class="text-decoration-none">Proofreading</a></li>
|
||||||
|
<li class="mb-2"><a href="#reading-aloud" class="text-decoration-none">Reading aloud</a></li>
|
||||||
|
<li class="mb-2"><a href="#using-ai-voices" class="text-decoration-none">Using AI voices</a></li>
|
||||||
|
<li class="mb-2"><a href="#common-things-to-watch-for" class="text-decoration-none">Things to watch for</a></li>
|
||||||
|
<li class="mb-2"><a href="#beta-readers-and-feedback" class="text-decoration-none">Beta readers</a></li>
|
||||||
|
<li class="mb-2"><a href="#editing-over-time" class="text-decoration-none">Why time helps</a></li>
|
||||||
|
<li><a href="#my-view-on-editing" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Editing improves the book. Proofreading cleans up what remains. Hearing the text aloud
|
||||||
|
is one of the best ways to catch what your eyes miss.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="text-decoration-none">
|
||||||
|
Reviews and ARC Readers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="text-decoration-none">
|
||||||
|
AI for Authors
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="text-decoration-none">
|
||||||
|
Cover Design
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ISBNs" class="btn btn-dark">
|
||||||
|
Previous: ISBNs
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="btn btn-dark">
|
||||||
|
Next: Cover Design
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
229
CatherineLynwood/Views/IndieAuthor/Index.cshtml
Normal file
229
CatherineLynwood/Views/IndieAuthor/Index.cshtml
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Indie Author and Self-Publishing Guide";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center mb-5">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
<div class="bg-dark rounded-4 p-4 p-md-5 shadow-sm border">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Indie Author and Self-Publishing</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
Self-publishing gives writers more control than ever before, but it also means learning how
|
||||||
|
the whole machine works... ISBNs, ebooks, print, audiobooks, pricing, advertising, reviews,
|
||||||
|
cover design, metadata, and all the fiddly little details nobody warns you about.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This section of the site is a practical guide based on real experience. Not theory, not recycled
|
||||||
|
fluff, and definitely not the fantasy that publishing one book will instantly turn life into a
|
||||||
|
champagne-fuelled author empire. It is intended as a growing resource for writers who want to
|
||||||
|
understand what self-publishing actually involves.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center mb-4">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Traditional vs Self-Publishing</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
A frank comparison of creative freedom, word count expectations, genre boundaries,
|
||||||
|
timescales, control, and the trade-offs involved.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">ISBNs</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
What ISBNs are, when you need them, how many to buy, what they cost, and how to
|
||||||
|
complete the registrations properly.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Isbns" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Editing and Proofreading</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
My editing process, including AI voices, listening back to chapters, catching
|
||||||
|
repetition, awkward phrasing, typos, and structural problems.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="EditingAndProofreading" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Cover Design</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Cover ideas, Photoshop, using AI for inspiration, what makes a strong cover, and why
|
||||||
|
many authors end up doing jobs they never wanted.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">KDP vs IngramSpark</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
The real-world differences between the two platforms, when to use one, when to use
|
||||||
|
both, and how distribution actually works.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Publishing on KDP</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Kindle publishing, KDP Select, Kindle Unlimited, royalties, setup screens, and what
|
||||||
|
Amazon gets right... and wrong.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Publishing on IngramSpark</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Hardbacks, paperbacks, file preparation, proof approvals, pricing, wholesale discount,
|
||||||
|
ecommerce links, and the ugly truth about returns.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Audiobooks and ACX</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Narrators, ACX, editing audio, chapter files, sound levels, proofing, and why audio
|
||||||
|
production is its own beast.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Audiobooks" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Reviews and ARC Readers</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Why reviews matter, how difficult they can be to get, the idea behind ARC readers,
|
||||||
|
and what actually happens in practice.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Amazon Advertising</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Pay per click, campaign types, wasted spend, realistic expectations, and why one book
|
||||||
|
alone usually does not make advertising profitable.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Author Finances</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Approximate costs, royalties, ad spend, profit margins, and the numbers behind indie
|
||||||
|
publishing, with the option to expand this into live figures later.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">AI for Authors</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Using AI as a practical creative tool, from image inspiration to voice playback and
|
||||||
|
idea testing, without handing over the actual writing.
|
||||||
|
</p>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AiForAuthors" class="btn btn-outline-dark">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
<div class="rounded-4 border p-4 p-md-5 bg-white text-black shadow-sm">
|
||||||
|
<h2 class="h3 fw-bold mb-3">A realistic note before you begin</h2>
|
||||||
|
<p class="mb-3">
|
||||||
|
Self-publishing can be brilliant. It gives writers control, flexibility, speed, and the ability
|
||||||
|
to build something entirely their own. But it also means wearing far too many hats, usually at
|
||||||
|
the same time.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Writing the book is only one part of the job. You may also end up acting as editor, formatter,
|
||||||
|
publisher, marketer, project manager, accountant, and reluctant amateur designer. That sounds a
|
||||||
|
bit grim, but the upside is that you get to make the decisions... and keep learning as you go.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
296
CatherineLynwood/Views/IndieAuthor/IngramSpark.cshtml
Normal file
296
CatherineLynwood/Views/IndieAuthor/IngramSpark.cshtml
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Publishing on IngramSpark";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Publishing on IngramSpark</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Publishing print books with IngramSpark</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
While Amazon KDP is often the easiest way to get a book onto Amazon, IngramSpark exists for a
|
||||||
|
slightly different reason. It is designed to distribute books into the wider book trade,
|
||||||
|
including bookshops, libraries and online retailers around the world.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In theory, it is the bridge between independent authors and the traditional retail ecosystem.
|
||||||
|
In practice, it introduces a few extra layers of complexity... pricing models, wholesale
|
||||||
|
discounts, returns policies, and a print setup that needs a little more care than simply
|
||||||
|
clicking “publish”.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="what-is-ingramspark" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What is IngramSpark?</h2>
|
||||||
|
<p>
|
||||||
|
IngramSpark is a publishing platform operated by <strong>Ingram</strong>, one of the
|
||||||
|
largest book distributors in the world. It allows publishers and independent authors
|
||||||
|
to upload print book files which can then be printed on demand and distributed through
|
||||||
|
the global Ingram distribution network.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That network includes:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>bookshops</li>
|
||||||
|
<li>libraries</li>
|
||||||
|
<li>online retailers</li>
|
||||||
|
<li>wholesale distributors</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
In other words, it is designed to make your book visible beyond Amazon’s own ecosystem.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="why-authors-use-ingram" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why indie authors use IngramSpark</h2>
|
||||||
|
<p>
|
||||||
|
Many indie authors use IngramSpark alongside KDP rather than instead of it. Each
|
||||||
|
platform does different things well.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The main reasons authors choose IngramSpark include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>wider print distribution</li>
|
||||||
|
<li>access to bookshops and libraries</li>
|
||||||
|
<li>hardback publishing options</li>
|
||||||
|
<li>global print-on-demand network</li>
|
||||||
|
<li>professional publishing metadata systems</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
For authors building a long-term catalogue, it can be an important piece of the
|
||||||
|
distribution puzzle.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="file-preparation" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Preparing your files</h2>
|
||||||
|
<p>
|
||||||
|
IngramSpark expects properly prepared print files. That usually means a
|
||||||
|
professionally formatted interior PDF and a correctly sized cover PDF including
|
||||||
|
spine width and bleed areas.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Typical requirements include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>correct trim size</li>
|
||||||
|
<li>proper margins and gutter</li>
|
||||||
|
<li>embedded fonts</li>
|
||||||
|
<li>high resolution images</li>
|
||||||
|
<li>cover sized precisely for page count</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
It is not complicated once you understand it, but it does reward careful preparation.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="proof-copies" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Proof copies and approvals</h2>
|
||||||
|
<p>
|
||||||
|
Before the book goes live, you normally approve a proof copy. This allows you to
|
||||||
|
check the printed version for layout errors, cover alignment, colour problems,
|
||||||
|
and other surprises that only appear once ink hits paper.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Things worth checking carefully include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>spine alignment</li>
|
||||||
|
<li>cover colour balance</li>
|
||||||
|
<li>page margins</li>
|
||||||
|
<li>image quality</li>
|
||||||
|
<li>typos that somehow survived editing</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Proofing properly can save a lot of embarrassment later.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="pricing-and-wholesale" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Pricing and wholesale discounts</h2>
|
||||||
|
<p>
|
||||||
|
This is where many new authors receive their first gentle introduction to the economics
|
||||||
|
of book publishing.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When publishing through IngramSpark you set:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>the retail price</li>
|
||||||
|
<li>the wholesale discount offered to retailers</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Bookshops expect a discount so they can make their margin. That discount typically
|
||||||
|
sits somewhere around the 40–55% range.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Once printing cost and discount are accounted for, the author’s share can be
|
||||||
|
surprisingly modest.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="returns" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The reality of book returns</h2>
|
||||||
|
<p>
|
||||||
|
Traditional book distribution includes the concept of <strong>returns</strong>.
|
||||||
|
Retailers can send unsold books back to the distributor and receive a refund.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
With IngramSpark you can choose whether to allow returns, but enabling them carries
|
||||||
|
financial risk. Returned books can be destroyed or shipped back to you, and the
|
||||||
|
cost ultimately falls on the publisher.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
It is one of those slightly uncomfortable truths of the publishing industry that
|
||||||
|
many cheerful “how to publish your book” guides prefer not to dwell on.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="distribution-network" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Distribution through the Ingram network</h2>
|
||||||
|
<p>
|
||||||
|
Once your book is active in the Ingram system, it becomes visible to thousands
|
||||||
|
of retailers through the distribution catalogue used by the book trade.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That does not mean bookshops will automatically stock it. It simply means they
|
||||||
|
can order it through the same supply chain they already use.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Discoverability still depends heavily on marketing, reputation and demand.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-ingram-gets-right" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What IngramSpark gets right</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>global distribution network</li>
|
||||||
|
<li>professional publishing infrastructure</li>
|
||||||
|
<li>strong print quality</li>
|
||||||
|
<li>hardback publishing options</li>
|
||||||
|
<li>integration with the wider book trade</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-ingram-gets-wrong" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What IngramSpark gets wrong</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>setup is more complex than KDP</li>
|
||||||
|
<li>pricing maths can be confusing for new authors</li>
|
||||||
|
<li>returns policies carry risk</li>
|
||||||
|
<li>support can sometimes feel distant</li>
|
||||||
|
<li>distribution does not guarantee bookshop placement</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-ingram" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on IngramSpark</h2>
|
||||||
|
<p>
|
||||||
|
IngramSpark is not the easiest publishing platform to learn, but it plays an
|
||||||
|
important role if you want your book available through the wider retail
|
||||||
|
ecosystem.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For many indie authors the most practical approach is to use KDP for Amazon
|
||||||
|
visibility while using IngramSpark for wider distribution and hardback
|
||||||
|
editions.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Like most parts of self-publishing, it rewards patience and careful setup
|
||||||
|
rather than rushing through the forms and hoping the universe sorts it out.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top:2rem">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#what-is-ingramspark">What is IngramSpark?</a></li>
|
||||||
|
<li class="mb-2"><a href="#why-authors-use-ingram">Why authors use it</a></li>
|
||||||
|
<li class="mb-2"><a href="#file-preparation">Preparing files</a></li>
|
||||||
|
<li class="mb-2"><a href="#proof-copies">Proof copies</a></li>
|
||||||
|
<li class="mb-2"><a href="#pricing-and-wholesale">Pricing and wholesale</a></li>
|
||||||
|
<li class="mb-2"><a href="#returns">Returns</a></li>
|
||||||
|
<li class="mb-2"><a href="#distribution-network">Distribution</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-ingram-gets-right">What it gets right</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-ingram-gets-wrong">What it gets wrong</a></li>
|
||||||
|
<li><a href="#my-view-on-ingram">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
IngramSpark is about distribution beyond Amazon, but it introduces
|
||||||
|
wholesale pricing, returns, and a few economic realities.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp">Publishing on KDP</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark">KDP vs IngramSpark</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances">Author finances</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp" class="btn btn-dark">
|
||||||
|
Previous: Publishing on KDP
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Audiobooks" class="btn btn-dark">
|
||||||
|
Next: Audiobooks and ACX
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
350
CatherineLynwood/Views/IndieAuthor/Isbns.cshtml
Normal file
350
CatherineLynwood/Views/IndieAuthor/Isbns.cshtml
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "ISBNs for Indie Authors";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">ISBNs</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">ISBNs, what they are and how indie authors use them</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
If you are publishing your own book, sooner or later you will come across ISBNs. They sound
|
||||||
|
mysterious and important, which is because they are... but they are not nearly as complicated as
|
||||||
|
some publishing guides make out.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page explains what an ISBN actually is, when you need one, how many you may need for a single
|
||||||
|
title, how registration works, and some of the practical realities indie authors run into when
|
||||||
|
dealing with print, ebook and audiobook formats.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="what-is-an-isbn" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What is an ISBN?</h2>
|
||||||
|
<p>
|
||||||
|
ISBN stands for <strong>International Standard Book Number</strong>. In simple terms, it is a
|
||||||
|
unique number used to identify a specific edition and format of a book.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That last part matters. An ISBN is not just attached to the story itself. It is attached to
|
||||||
|
a particular <strong>version</strong> of that book. So if you publish the same title in
|
||||||
|
paperback, hardback and ebook, those are generally treated as separate products.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Think of it less like the identity of the novel and more like the identity of a specific
|
||||||
|
published format of that novel.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="when-do-you-need-an-isbn" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">When do you need an ISBN?</h2>
|
||||||
|
<p>
|
||||||
|
In practice, you usually need an ISBN for printed editions such as paperbacks and hardbacks.
|
||||||
|
Whether you need one for an ebook depends on the platform you are using and how much control
|
||||||
|
you want over your publishing setup.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Format</th>
|
||||||
|
<th>Usually needs its own ISBN?</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Paperback</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>Normally requires its own ISBN.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Hardback</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>Separate from the paperback edition.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Kindle ebook</td>
|
||||||
|
<td>Not always</td>
|
||||||
|
<td>Amazon can list Kindle books without you supplying your own ISBN.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>EPUB on other platforms</td>
|
||||||
|
<td>Often yes</td>
|
||||||
|
<td>Depends on distributor and how widely you are publishing.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Audiobook</td>
|
||||||
|
<td>Sometimes</td>
|
||||||
|
<td>Depends on the platform and distribution route.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
The key thing is this: if you are releasing your book in more than one format, do not assume
|
||||||
|
one ISBN covers the lot. It usually does not.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="how-many-isbns" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">How many ISBNs might one book need?</h2>
|
||||||
|
<p>
|
||||||
|
This catches a lot of new authors out. One title can easily use multiple ISBNs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For example, a single novel might need:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>one ISBN for the paperback</li>
|
||||||
|
<li>one ISBN for the hardback</li>
|
||||||
|
<li>one ISBN for a wide-distribution ebook edition</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
So even if you are only publishing one book, you may want more than one ISBN available from
|
||||||
|
the start. Buy too few and you can end up going back for more sooner than expected, which is
|
||||||
|
mildly annoying at best and a complete faff at worst.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="buying-isbns" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Buying ISBNs</h2>
|
||||||
|
<p>
|
||||||
|
In the UK, authors normally buy ISBNs from the official national ISBN agency. Other
|
||||||
|
countries have their own agencies and systems.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What matters most is buying them from the correct official source for your country, rather
|
||||||
|
than from some random middleman trying to look useful while charging extra for the privilege.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you are planning more than one format, or more than one book, it often makes sense to buy
|
||||||
|
a block rather than a single ISBN. That gives you flexibility later and avoids having to redo
|
||||||
|
the whole mental calculation every time you add a new edition.
|
||||||
|
</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>1 Single ISBN</th>
|
||||||
|
<th>Prefix for 10 ISBNs</th>
|
||||||
|
<th>Prefix for 100 ISBNs</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>£93 per ISBN</td>
|
||||||
|
<td>£174 per block of 10</td>
|
||||||
|
<td>£387 per block of 100</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0">
|
||||||
|
The prices above were correct at the time of publication, however you
|
||||||
|
can find current pricing and order your ISBNs on the <a href="https://www.nielsenisbnstore.com/Home/Isbn" target="_blank">NielsenIQ BookData website</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="registering-isbns" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Registering each ISBN properly</h2>
|
||||||
|
<p>
|
||||||
|
Buying an ISBN is only part of the job. You also need to register it correctly against the
|
||||||
|
right book format and metadata.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Although you can buy the ISBNs within seconds, it takes quite a lot longer to be able to
|
||||||
|
actually register them. To do this you have to wait for an account to be set up on the
|
||||||
|
<a href="https://www.nielsentitleeditor.com/titleeditor/" target="_blank">NelisenIQ Title Editor</a>.
|
||||||
|
Now it appears that this account creation is performed manually, and takes approximately
|
||||||
|
two weeks to come through, so you will need to keep this in mind. That said you do not need to do this
|
||||||
|
part until you have your book files ready for submission to your chosen platforms.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That normally includes details such as:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>title and subtitle</li>
|
||||||
|
<li>author or pen name</li>
|
||||||
|
<li>format and binding type</li>
|
||||||
|
<li>publication date</li>
|
||||||
|
<li>price</li>
|
||||||
|
<li>publisher name or imprint</li>
|
||||||
|
<li>the page count for the book</li>
|
||||||
|
<li>the number of images, if any, and what type</li>
|
||||||
|
<li>the physical dimensions of the book</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
This is worth doing carefully. Metadata errors can create confusion later, especially once
|
||||||
|
books begin appearing across retailers, databases and library systems.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In other words, this is not the glamorous part of publishing, but it is absolutely one of
|
||||||
|
the bits that benefits from getting it right first time. Don't worry if you do make a mistake though,
|
||||||
|
you can edit this data once it's live on the system. obviously it's better to get it right first time.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="amazon-kdp-isbns" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Do Amazon and KDP give you an ISBN?</h2>
|
||||||
|
<p>
|
||||||
|
For some formats, Amazon can provide an identifier or allow publication without you using
|
||||||
|
your own purchased ISBN. That can be convenient, especially when starting out as it's a free service.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The trade-off is control. Using platform-provided identifiers can tie that edition more
|
||||||
|
closely to the platform that issued it. If you want a cleaner, more independent publishing
|
||||||
|
setup, owning your own ISBNs is often the better route.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Convenience is nice. Control is nicer. It just depends how seriously you are treating the
|
||||||
|
book and how far you plan to take it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="common-mistakes" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Common mistakes to avoid</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Assuming one ISBN covers every format</li>
|
||||||
|
<li>Buying too few and then needing more almost immediately</li>
|
||||||
|
<li>Registering the wrong format against the wrong number</li>
|
||||||
|
<li>Forgetting to keep a clear record of which ISBN belongs to which edition</li>
|
||||||
|
<li>Treating metadata as an afterthought</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view as an indie author</h2>
|
||||||
|
<p>
|
||||||
|
ISBNs are one of those things that seem intimidating before you start, then turn out to be
|
||||||
|
mostly admin once you understand the rules. They are not exciting, but they do matter.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I chose to purchase a block of 10 ISBNs. You're almost certainly going to need more than one,
|
||||||
|
however in heindsight I wish I'd bought a block of 100. If you're going to write more than 2 or 3
|
||||||
|
books then it's the cheapest option over time.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For my first book I used up 4 ISBNs:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
1 for the eBook. Initially I thought I would use multiple distribution platforms, so it
|
||||||
|
made sense. However after a few months I settled on KDP and Kindle Select which gives you
|
||||||
|
access to Kindle Unlimited
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
1 for the Amazon KDP paperback. For my second book I'm not doing this due to the way Amazon
|
||||||
|
groups editions of my book
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
1 for my IngramSpark paperback, or what I call Bookshop Edition
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
1 for my IngramSpark hardback, or what I call my Collector's Edition
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
I didn't use one for the audiobook. I found out i didn't need to so I didn't
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
For indie authors, the real question is less “what is an ISBN?” and more “how many formats
|
||||||
|
am I realistically going to publish, and how much control do I want over them?”
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Answer that properly and the ISBN decision becomes much simpler.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#what-is-an-isbn" class="text-decoration-none">What is an ISBN?</a></li>
|
||||||
|
<li class="mb-2"><a href="#when-do-you-need-an-isbn" class="text-decoration-none">When do you need one?</a></li>
|
||||||
|
<li class="mb-2"><a href="#how-many-isbns" class="text-decoration-none">How many might you need?</a></li>
|
||||||
|
<li class="mb-2"><a href="#buying-isbns" class="text-decoration-none">Buying ISBNs</a></li>
|
||||||
|
<li class="mb-2"><a href="#registering-isbns" class="text-decoration-none">Registering each one</a></li>
|
||||||
|
<li class="mb-2"><a href="#amazon-kdp-isbns" class="text-decoration-none">Amazon and KDP</a></li>
|
||||||
|
<li><a href="#common-mistakes" class="text-decoration-none">Common mistakes</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
One format usually means one ISBN. Paperback, hardback and ebook editions often
|
||||||
|
need to be treated separately.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp" class="text-decoration-none">
|
||||||
|
Publishing on KDP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
|
||||||
|
Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
|
||||||
|
KDP vs IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="TraditionalVsSelfPublishing" class="btn btn-dark">
|
||||||
|
Previous: Traditional vs Self-Publishing
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="EditingAndProofReading" class="btn btn-dark">
|
||||||
|
Next: Editing and Proofreading
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
383
CatherineLynwood/Views/IndieAuthor/Kdp.cshtml
Normal file
383
CatherineLynwood/Views/IndieAuthor/Kdp.cshtml
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Publishing on Amazon KDP";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Publishing on KDP</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Publishing on Amazon KDP</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
For many indie authors, Amazon KDP is the first real step into publishing. It is accessible,
|
||||||
|
relatively quick to use, and gives authors a direct route to publishing ebooks and printed books
|
||||||
|
without begging for permission from gatekeepers who think every novel on earth ought to be 80,000
|
||||||
|
words and neatly shoved into one marketable box.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This page looks at what KDP is, how it works, what you can publish through it, the pros and cons
|
||||||
|
of Kindle Unlimited, and some of the practical realities of dealing with setup screens, royalties,
|
||||||
|
categories, keywords, and all the little details that make a difference.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="what-is-kdp" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What is KDP?</h2>
|
||||||
|
<p>
|
||||||
|
<strong>KDP</strong>, or <strong>Kindle Direct Publishing</strong>, is Amazon’s self-publishing
|
||||||
|
platform. It allows authors and publishers to upload books directly to Amazon and sell them
|
||||||
|
without going through a traditional publishing house.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For indie authors, it is one of the easiest ways to get a book in front of readers. You can
|
||||||
|
publish ebooks for Kindle, and you can also publish print editions such as paperbacks and,
|
||||||
|
in some cases, hardbacks.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The big attraction is speed and control. You make the decisions, you upload the files, you
|
||||||
|
set the pricing, and you retain far more ownership over the process than you ever would in
|
||||||
|
traditional publishing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="why-authors-use-kdp" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why authors use KDP</h2>
|
||||||
|
<p>
|
||||||
|
KDP is popular because it removes a lot of barriers. You do not need an agent, you do not
|
||||||
|
need a publishing deal, and you do not need somebody in London to decide whether your book
|
||||||
|
is currently fashionable enough for them to care.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Some of the main reasons indie authors use KDP include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>easy access to the Amazon marketplace</li>
|
||||||
|
<li>fast setup compared with traditional publishing</li>
|
||||||
|
<li>control over pricing and metadata</li>
|
||||||
|
<li>direct publishing of Kindle ebooks</li>
|
||||||
|
<li>the option to create paperback editions</li>
|
||||||
|
<li>built-in connection to Amazon sales pages and reviews</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
It is not perfect, but it is often the most obvious place to begin, especially if your goal
|
||||||
|
is simply to get the book live and available to buy.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-you-can-publish" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What you can publish through KDP</h2>
|
||||||
|
<p>
|
||||||
|
KDP is most strongly associated with Kindle ebooks, but it can also handle certain print
|
||||||
|
formats. That makes it useful for authors who want to keep ebook and print publishing under
|
||||||
|
one roof, at least to begin with.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Format</th>
|
||||||
|
<th>Supported by KDP?</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Kindle ebook</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>The core KDP format and usually the easiest to publish.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Paperback</td>
|
||||||
|
<td>Yes</td>
|
||||||
|
<td>Print-on-demand through Amazon.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Hardback</td>
|
||||||
|
<td>Sometimes</td>
|
||||||
|
<td>Available in some setups, but many indie authors still prefer IngramSpark for hardbacks.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Audiobook</td>
|
||||||
|
<td>No</td>
|
||||||
|
<td>Audiobooks are handled separately, often through ACX rather than KDP itself.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
In practice, many indie authors use KDP for ebooks and Amazon print visibility, then use
|
||||||
|
other services for wider print distribution or audio.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="setting-up-a-book" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Setting up a book on KDP</h2>
|
||||||
|
<p>
|
||||||
|
KDP walks you through the setup in stages. On the surface, it all looks fairly simple:
|
||||||
|
title, description, keywords, categories, manuscript upload, cover upload, price, done.
|
||||||
|
In reality, each of those boxes has the power to help or quietly sabotage your book.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A typical KDP setup includes:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>book title and subtitle</li>
|
||||||
|
<li>series information, if relevant</li>
|
||||||
|
<li>author name or pen name</li>
|
||||||
|
<li>book description</li>
|
||||||
|
<li>publishing rights confirmation</li>
|
||||||
|
<li>keywords and categories</li>
|
||||||
|
<li>age or audience information where needed</li>
|
||||||
|
<li>manuscript upload</li>
|
||||||
|
<li>cover upload or use of Amazon’s cover tools</li>
|
||||||
|
<li>pricing and territory settings</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
None of this is especially difficult, but it does reward care. Sloppy metadata and rushed
|
||||||
|
presentation make a book look amateur before anyone has even read page one.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="kindle-ebooks" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Kindle ebooks</h2>
|
||||||
|
<p>
|
||||||
|
For many authors, the ebook edition is the easiest place to start. It does not involve print
|
||||||
|
costs, there is no physical proof copy to approve, and updating the file is generally less
|
||||||
|
painful than with print.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Kindle also gives you access to Amazon’s enormous customer base, which is obviously the bit
|
||||||
|
everyone likes. The less glamorous bit is that it also means competing with approximately a
|
||||||
|
million other books and a small mountain of algorithm-driven nonsense.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Still, ebooks are often the lowest-friction way to get a book selling online, and for many
|
||||||
|
indie authors they remain the main entry point into the market.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="kindle-unlimited" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Kindle Unlimited and KDP Select</h2>
|
||||||
|
<p>
|
||||||
|
KDP Select is Amazon’s optional exclusivity programme for ebooks. If you enrol your Kindle
|
||||||
|
book in it, that ebook generally has to remain exclusive to Amazon for the enrolment period.
|
||||||
|
In return, the book can be included in <strong>Kindle Unlimited</strong>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Kindle Unlimited allows subscribers to read books from the programme as part of their
|
||||||
|
membership. Authors are then paid based on pages read rather than only on direct purchases.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That sounds lovely in theory. In practice, it depends heavily on the type of book, your
|
||||||
|
audience, your pricing strategy, and whether being exclusive to Amazon suits your wider
|
||||||
|
publishing plans.
|
||||||
|
</p>
|
||||||
|
<div class="card border-0 bg-light rounded-4 mt-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="h5 fw-bold mb-3">Things to think about with Kindle Unlimited</h3>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>You gain access to Kindle Unlimited readers</li>
|
||||||
|
<li>You are usually giving Amazon ebook exclusivity for that period</li>
|
||||||
|
<li>You may earn from page reads rather than only book sales</li>
|
||||||
|
<li>It can help visibility for some genres more than others</li>
|
||||||
|
<li>It may not suit authors who want their ebooks wide on multiple stores</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="paperbacks-and-print" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Paperbacks and print-on-demand</h2>
|
||||||
|
<p>
|
||||||
|
KDP can also handle paperback publishing through print-on-demand. That means copies are
|
||||||
|
printed when ordered, rather than you having to buy and store a garage full of books like a
|
||||||
|
literary greengrocer.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is incredibly useful for indie authors because it lowers the financial risk. You do not
|
||||||
|
need a giant print run upfront. You simply upload the interior and cover files, choose the
|
||||||
|
relevant trim and print settings, then approve the proofing stage.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Print-on-demand is one of the reasons self-publishing is even viable for so many authors now.
|
||||||
|
The downside is that print cost and retail margin can squeeze profit pretty hard, especially
|
||||||
|
on long books.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="keywords-categories-and-metadata" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Keywords, categories and metadata</h2>
|
||||||
|
<p>
|
||||||
|
This is one of the least exciting but most important parts of KDP. Your book description,
|
||||||
|
categories, keywords, subtitle, and general metadata all affect how discoverable the book is
|
||||||
|
and how accurately Amazon understands what it is.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you pick lazy categories or vague keywords, you make life harder for yourself. The same
|
||||||
|
goes for weak descriptions. You can write a brilliant novel and still trip over your own
|
||||||
|
shoelaces at the metadata stage.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Good metadata will not magically sell a bad book, but bad metadata can absolutely bury a
|
||||||
|
good one.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="royalties-and-pricing" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Royalties and pricing</h2>
|
||||||
|
<p>
|
||||||
|
One of the first things new authors notice on KDP is that pricing is tied closely to royalty
|
||||||
|
structures. In ebooks especially, price can affect which royalty option applies. Print books
|
||||||
|
then add printing cost on top, which can make the maths much less glamorous than people hope.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The important thing is not to price purely based on ego. Your book may well deserve to be
|
||||||
|
treated like a masterpiece, but readers still compare it with everything else on the store.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Pricing is part positioning, part experiment, and part cold acceptance that profit per book
|
||||||
|
is often thinner than outsiders imagine.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-kdp-gets-right" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What KDP gets right</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>It is easy to access and reasonably straightforward to use</li>
|
||||||
|
<li>It gets books onto Amazon quickly</li>
|
||||||
|
<li>It supports both ebook and print workflows</li>
|
||||||
|
<li>It gives indie authors direct control over publishing decisions</li>
|
||||||
|
<li>It lowers the barrier to entry dramatically</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-kdp-gets-wrong" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What KDP gets wrong</h2>
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>It encourages dependence on a single giant retailer</li>
|
||||||
|
<li>Its systems can feel rigid or opaque in places</li>
|
||||||
|
<li>Metadata and category control is not always as flexible as authors would like</li>
|
||||||
|
<li>Kindle Unlimited exclusivity can be limiting</li>
|
||||||
|
<li>It can make publishing look easier than marketing actually is</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view-on-kdp" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view on KDP as an indie author</h2>
|
||||||
|
<p>
|
||||||
|
KDP is a very useful tool. It is not a magic wand, not a publishing strategy by itself, and
|
||||||
|
definitely not a guarantee of sales. But it is one of the most practical ways for an indie
|
||||||
|
author to get a book into the world.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The real danger is assuming that uploading the book is the finish line. It is not. It is the
|
||||||
|
start of the next phase, which involves presentation, discoverability, reviews, pricing,
|
||||||
|
advertising, and slowly building a catalogue that gives readers somewhere else to go if they
|
||||||
|
like your work.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In other words, KDP can publish your book. It cannot build your career for you. Bit rude of
|
||||||
|
it really.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#what-is-kdp" class="text-decoration-none">What is KDP?</a></li>
|
||||||
|
<li class="mb-2"><a href="#why-authors-use-kdp" class="text-decoration-none">Why authors use it</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-you-can-publish" class="text-decoration-none">What you can publish</a></li>
|
||||||
|
<li class="mb-2"><a href="#setting-up-a-book" class="text-decoration-none">Setting up a book</a></li>
|
||||||
|
<li class="mb-2"><a href="#kindle-ebooks" class="text-decoration-none">Kindle ebooks</a></li>
|
||||||
|
<li class="mb-2"><a href="#kindle-unlimited" class="text-decoration-none">Kindle Unlimited</a></li>
|
||||||
|
<li class="mb-2"><a href="#paperbacks-and-print" class="text-decoration-none">Paperbacks and print</a></li>
|
||||||
|
<li class="mb-2"><a href="#keywords-categories-and-metadata" class="text-decoration-none">Metadata</a></li>
|
||||||
|
<li class="mb-2"><a href="#royalties-and-pricing" class="text-decoration-none">Royalties and pricing</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-kdp-gets-right" class="text-decoration-none">What KDP gets right</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-kdp-gets-wrong" class="text-decoration-none">What KDP gets wrong</a></li>
|
||||||
|
<li><a href="#my-view-on-kdp" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
KDP is one of the easiest ways for indie authors to publish ebooks and print books,
|
||||||
|
but getting listed is only the beginning. Visibility is the real battle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Isbns" class="text-decoration-none">
|
||||||
|
ISBNs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="text-decoration-none">
|
||||||
|
Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
|
||||||
|
KDP vs IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="btn btn-dark">
|
||||||
|
Previous: KDP vs IngramSpark
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark" class="btn btn-dark">
|
||||||
|
Next: Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
377
CatherineLynwood/Views/IndieAuthor/KdpVsIngramSpark.cshtml
Normal file
377
CatherineLynwood/Views/IndieAuthor/KdpVsIngramSpark.cshtml
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "KDP vs IngramSpark";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active">KDP vs IngramSpark</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">KDP vs IngramSpark</h1>
|
||||||
|
|
||||||
|
<p class="lead mb-3">
|
||||||
|
One of the most common questions new indie authors ask is whether they should publish their print
|
||||||
|
books through Amazon KDP or through IngramSpark.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
The honest answer is that they serve slightly different purposes. Many independent authors
|
||||||
|
eventually end up using both platforms together, each handling the parts of publishing they
|
||||||
|
do best.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="the-short-answer" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The short answer</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If your primary goal is selling books on Amazon, KDP is usually the easiest and most
|
||||||
|
straightforward route.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you want your book available through bookshops, libraries and the wider book trade,
|
||||||
|
IngramSpark provides access to the global distribution network used by retailers.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
For many authors the practical solution is to combine the two.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="side-by-side-comparison" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Side-by-side comparison</h2>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
|
||||||
|
<table class="table table-bordered align-middle">
|
||||||
|
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Feature</th>
|
||||||
|
<th>KDP</th>
|
||||||
|
<th>IngramSpark</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Primary purpose</td>
|
||||||
|
<td>Amazon publishing platform</td>
|
||||||
|
<td>Global book distribution network</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Ebooks</td>
|
||||||
|
<td>Excellent Kindle support</td>
|
||||||
|
<td>Not its main focus</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Paperbacks</td>
|
||||||
|
<td>Simple print-on-demand</td>
|
||||||
|
<td>Professional trade distribution</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Hardbacks</td>
|
||||||
|
<td>Available in some cases</td>
|
||||||
|
<td>Strong support for hardback formats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Distribution</td>
|
||||||
|
<td>Primarily Amazon</td>
|
||||||
|
<td>Bookshops, libraries, retailers worldwide</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Setup complexity</td>
|
||||||
|
<td>Very straightforward</td>
|
||||||
|
<td>More detailed setup</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Wholesale discount</td>
|
||||||
|
<td>Not required</td>
|
||||||
|
<td>Required for trade distribution</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Returns system</td>
|
||||||
|
<td>No traditional return system</td>
|
||||||
|
<td>Retail returns possible</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>Best for</td>
|
||||||
|
<td>Amazon-focused publishing</td>
|
||||||
|
<td>Trade distribution and hardbacks</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="when-kdp-makes-sense" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">When KDP makes the most sense</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
For many indie authors, KDP is the simplest way to get started.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>you want fast publishing</li>
|
||||||
|
<li>Amazon is your main sales channel</li>
|
||||||
|
<li>you want simple setup</li>
|
||||||
|
<li>you want Kindle ebook support</li>
|
||||||
|
<li>you prefer minimal distribution complexity</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
It removes a lot of friction from the publishing process and allows authors to focus on writing,
|
||||||
|
presentation and marketing.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="when-ingram-makes-sense" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">When IngramSpark makes sense</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
IngramSpark becomes valuable when authors want access to the wider book trade.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>bookshop availability</li>
|
||||||
|
<li>library distribution</li>
|
||||||
|
<li>global retail catalogues</li>
|
||||||
|
<li>hardback editions</li>
|
||||||
|
<li>wider publishing infrastructure</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
It is particularly useful for authors building a catalogue or publishing imprint rather than
|
||||||
|
simply uploading a single book.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="using-both-platforms" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Using both platforms together</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A common strategy among indie authors is to combine both services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Typical setups look something like this:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>KDP for Kindle ebooks</li>
|
||||||
|
<li>KDP for Amazon print visibility</li>
|
||||||
|
<li>IngramSpark for wider distribution</li>
|
||||||
|
<li>IngramSpark for hardback editions</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
Each platform then handles the parts it is best at rather than forcing one system to do everything.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="the-bookshop-myth" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The bookshop myth</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Many authors assume that once a book is listed in the Ingram distribution network, bookshops
|
||||||
|
will automatically stock it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Unfortunately that is not how it works.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bookshops stock books they believe will sell. Being available through Ingram simply means the
|
||||||
|
book can be ordered through their normal supply chain. It does not guarantee shelf space.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
Distribution creates opportunity. It does not create demand.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="the-financial-reality" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">The financial reality</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Both platforms involve economic trade-offs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>KDP print margins are often modest</li>
|
||||||
|
<li>IngramSpark requires wholesale discounts</li>
|
||||||
|
<li>print cost increases with book length</li>
|
||||||
|
<li>returns policies can introduce risk</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
Understanding the numbers is an important part of publishing decisions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section id="my-view" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My view as an indie author</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
KDP and IngramSpark are not really competing services. They are different tools
|
||||||
|
within the same publishing toolbox.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
KDP is excellent for fast publishing and Amazon visibility.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
IngramSpark is valuable for broader distribution and professional publishing
|
||||||
|
infrastructure.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-0">
|
||||||
|
Using both together often provides the most flexibility for indie authors building
|
||||||
|
a long-term catalogue.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
|
||||||
|
<div class="sticky-lg-top" style="top:2rem">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
|
||||||
|
<li class="mb-2"><a href="#the-short-answer">The short answer</a></li>
|
||||||
|
<li class="mb-2"><a href="#side-by-side-comparison">Comparison table</a></li>
|
||||||
|
<li class="mb-2"><a href="#when-kdp-makes-sense">When KDP makes sense</a></li>
|
||||||
|
<li class="mb-2"><a href="#when-ingram-makes-sense">When IngramSpark makes sense</a></li>
|
||||||
|
<li class="mb-2"><a href="#using-both-platforms">Using both platforms</a></li>
|
||||||
|
<li class="mb-2"><a href="#the-bookshop-myth">The bookshop myth</a></li>
|
||||||
|
<li class="mb-2"><a href="#the-financial-reality">Financial reality</a></li>
|
||||||
|
<li><a href="#my-view">My view</a></li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
|
||||||
|
<p class="small mb-0">
|
||||||
|
KDP is ideal for Amazon publishing. IngramSpark extends your reach into
|
||||||
|
the wider book trade. Many authors use both.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp">
|
||||||
|
Publishing on KDP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="IngramSpark">
|
||||||
|
Publishing on IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AuthorFinances">
|
||||||
|
Author finances
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="CoverDesign" class="btn btn-dark">
|
||||||
|
Previous: Cover Design
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Kdp" class="btn btn-dark">
|
||||||
|
Next: Publishing on KDP
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
275
CatherineLynwood/Views/IndieAuthor/ReviewsAndArcReaders.cshtml
Normal file
275
CatherineLynwood/Views/IndieAuthor/ReviewsAndArcReaders.cshtml
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Reviews and ARC Readers";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Reviews and ARC Readers</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Reviews and ARC readers</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
Reviews are one of the most important pieces of social proof on Amazon and other book retailers.
|
||||||
|
When a reader lands on your book page, they often glance at the rating and number of reviews
|
||||||
|
before they even read the blurb.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Unfortunately, getting reviews is not easy. Even readers who genuinely enjoyed your book often
|
||||||
|
forget to leave one. This page looks at why reviews matter, what ARC readers are, and some of
|
||||||
|
the realities indie authors face when trying to build early feedback for their work.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="why-reviews-matter" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why reviews matter</h2>
|
||||||
|
<p>
|
||||||
|
Reviews serve two main purposes. First, they help potential readers decide whether the
|
||||||
|
book is worth their time and money. Second, they feed into the algorithms that determine
|
||||||
|
how visible a book becomes on retail platforms like Amazon.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A book with no reviews often looks risky to a new reader. A book with a handful of
|
||||||
|
thoughtful reviews feels safer. Once a book gathers more reviews, it begins to look
|
||||||
|
established.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
That perception matters a lot more than authors sometimes realise.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="how-readers-use-reviews" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">How readers actually use reviews</h2>
|
||||||
|
<p>
|
||||||
|
Most readers don’t analyse reviews in great detail. Instead they tend to scan quickly.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>They glance at the star rating</li>
|
||||||
|
<li>They check roughly how many reviews there are</li>
|
||||||
|
<li>They skim one or two short comments</li>
|
||||||
|
<li>They look for signs the book fits their taste</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Reviews therefore work as a form of reassurance. Even a short comment like
|
||||||
|
“I really enjoyed this story” can help signal that the book has already been read
|
||||||
|
and appreciated by someone else.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
A completely empty review section, on the other hand, can make a book feel invisible.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="what-arc-readers-are" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">What ARC readers are</h2>
|
||||||
|
<p>
|
||||||
|
ARC stands for <strong>Advance Review Copy</strong>. These are early copies of a book
|
||||||
|
given to readers before publication in the hope they will leave honest reviews once
|
||||||
|
the book is released.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
ARC readers might receive:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>a digital manuscript</li>
|
||||||
|
<li>a pre-release ebook</li>
|
||||||
|
<li>a printed proof copy</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
The idea is simple. Early readers help seed the review section so the book does not
|
||||||
|
launch into the world looking completely empty.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="finding-arc-readers" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Finding ARC readers</h2>
|
||||||
|
<p>
|
||||||
|
In theory this sounds straightforward. In practice it can be surprisingly difficult.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Many authors try to recruit ARC readers from:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>mailing lists</li>
|
||||||
|
<li>social media followers</li>
|
||||||
|
<li>reader communities</li>
|
||||||
|
<li>genre-specific groups</li>
|
||||||
|
<li>friends and early supporters</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
Even then, not everyone who accepts an ARC will actually leave a review. Some readers
|
||||||
|
simply forget, get busy, or never quite finish the book.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="managing-expectations" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Managing expectations</h2>
|
||||||
|
<p>
|
||||||
|
One of the realities indie authors eventually learn is that only a small percentage
|
||||||
|
of readers leave reviews.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Even enthusiastic readers often finish a book, close the app, and move on with their
|
||||||
|
day. Writing a review takes time and effort, and many readers simply don’t think to do it.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
This means that a modest number of reviews does not necessarily reflect a lack of
|
||||||
|
readership. It often just reflects normal human behaviour.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="honest-reviews" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Why honest reviews matter</h2>
|
||||||
|
<p>
|
||||||
|
ARC readers should always be encouraged to leave honest reviews rather than positive
|
||||||
|
ones. The purpose is not to manufacture praise but to give readers genuine feedback
|
||||||
|
from people who have read the book.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Honest reviews build credibility. If every review sounds suspiciously identical or
|
||||||
|
overly enthusiastic, readers may become sceptical.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
A mix of reactions is normal and healthy.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="amazon-rules" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Amazon review rules</h2>
|
||||||
|
<p>
|
||||||
|
Amazon has strict policies around reviews. Authors should avoid anything that could
|
||||||
|
be interpreted as manipulating the review system.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Examples of things to avoid include:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>paying for positive reviews</li>
|
||||||
|
<li>review swaps between authors</li>
|
||||||
|
<li>asking friends or family to leave reviews without disclosure</li>
|
||||||
|
<li>offering rewards for favourable reviews</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
The safest approach is simply to ask readers to leave an honest review if they enjoyed
|
||||||
|
the book.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="how-to-encourage-reviews" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Encouraging readers to leave reviews</h2>
|
||||||
|
<p>
|
||||||
|
While you cannot force readers to leave reviews, you can make it easier for them.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Include a short request at the end of the book</li>
|
||||||
|
<li>Thank readers for their support</li>
|
||||||
|
<li>Provide a simple reminder that reviews help authors</li>
|
||||||
|
<li>Keep the request brief and polite</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-0">
|
||||||
|
A gentle reminder is often enough to nudge a few readers into leaving feedback.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-experience-with-reviews" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My experience with reviews</h2>
|
||||||
|
<p>
|
||||||
|
In my own experience, gathering reviews can be slower than you expect. Even when readers
|
||||||
|
say they enjoyed the book, that doesn’t always translate into a written review on the
|
||||||
|
retailer page.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
That can feel frustrating, but it’s part of the normal rhythm of publishing. Reviews
|
||||||
|
tend to accumulate gradually over time rather than appearing all at once.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
The best approach is to focus on writing the next book while the reviews slowly build
|
||||||
|
in the background.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#why-reviews-matter" class="text-decoration-none">Why reviews matter</a></li>
|
||||||
|
<li class="mb-2"><a href="#how-readers-use-reviews" class="text-decoration-none">How readers use reviews</a></li>
|
||||||
|
<li class="mb-2"><a href="#what-arc-readers-are" class="text-decoration-none">What ARC readers are</a></li>
|
||||||
|
<li class="mb-2"><a href="#finding-arc-readers" class="text-decoration-none">Finding ARC readers</a></li>
|
||||||
|
<li class="mb-2"><a href="#managing-expectations" class="text-decoration-none">Managing expectations</a></li>
|
||||||
|
<li class="mb-2"><a href="#honest-reviews" class="text-decoration-none">Honest reviews</a></li>
|
||||||
|
<li class="mb-2"><a href="#amazon-rules" class="text-decoration-none">Amazon rules</a></li>
|
||||||
|
<li class="mb-2"><a href="#how-to-encourage-reviews" class="text-decoration-none">Encouraging reviews</a></li>
|
||||||
|
<li><a href="#my-experience-with-reviews" class="text-decoration-none">My experience</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Reviews build trust with readers and improve visibility, but they usually
|
||||||
|
accumulate slowly. Even enthusiastic readers often forget to leave them.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="text-decoration-none">
|
||||||
|
Amazon Advertising
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="EditingAndProofreading" class="text-decoration-none">
|
||||||
|
Editing and Proofreading
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index" class="text-decoration-none">
|
||||||
|
Guide Index
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Audiobooks" class="btn btn-dark">
|
||||||
|
Previous: Audiobooks and ACX
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="AmazonAdvertising" class="btn btn-dark">
|
||||||
|
Next: Amazon Advertising
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,310 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Traditional Publishing vs Self Publishing";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-10">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb" class="mb-4">
|
||||||
|
<ol class="breadcrumb small">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index">Indie Author Guide</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Traditional vs Self Publishing</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="mb-5">
|
||||||
|
<span class="badge text-bg-dark mb-3">Indie Author Guide</span>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Traditional publishing vs self publishing</h1>
|
||||||
|
<p class="lead mb-3">
|
||||||
|
For decades traditional publishing was the only realistic route into the book industry.
|
||||||
|
Authors would submit manuscripts to literary agents, agents would approach publishers,
|
||||||
|
and if everything aligned the book might eventually appear in print.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Today things are very different. Platforms like Amazon KDP and IngramSpark allow authors
|
||||||
|
to publish their work directly. That doesn’t make one path automatically better than the
|
||||||
|
other. Each route has advantages, disadvantages, and trade-offs that authors need to
|
||||||
|
understand before deciding which direction suits them.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
|
||||||
|
<section id="traditional-publishing-overview" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">How traditional publishing works</h2>
|
||||||
|
<p>
|
||||||
|
Traditional publishing usually involves several stages before a book ever reaches
|
||||||
|
readers. The author submits their manuscript to literary agents. If an agent
|
||||||
|
believes the work has commercial potential they may agree to represent the author.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The agent then approaches publishing houses in the hope that one of them will
|
||||||
|
acquire the book. If accepted, the publisher typically handles editing, cover
|
||||||
|
design, printing, and distribution.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
In return, the author receives a royalty on sales and often gives the publisher
|
||||||
|
certain rights to the work for a period of time.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="traditional-pros" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Advantages of traditional publishing</h2>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Professional editorial support</li>
|
||||||
|
<li>Established distribution networks</li>
|
||||||
|
<li>Access to physical bookshops</li>
|
||||||
|
<li>Marketing support from the publisher</li>
|
||||||
|
<li>Industry recognition and credibility</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-3 mb-0">
|
||||||
|
For some authors, particularly those seeking large commercial exposure or literary
|
||||||
|
recognition, these advantages can be significant.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="traditional-cons" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Challenges with traditional publishing</h2>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Very competitive submission process</li>
|
||||||
|
<li>Long timelines before publication</li>
|
||||||
|
<li>Less control over the final product</li>
|
||||||
|
<li>Lower royalty percentages</li>
|
||||||
|
<li>Pressure to fit established genre expectations</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-3 mb-0">
|
||||||
|
The submission process alone can take months or even years, and many manuscripts
|
||||||
|
never reach publication through this route. It's not unheard of for it to take two
|
||||||
|
years or more for your work to get published.
|
||||||
|
</p>
|
||||||
|
<p class="mt-3 mb-0">
|
||||||
|
Additionally don't think that if you do manage to get traditionally published you'll
|
||||||
|
then get to sit back and enjoy the ride. You'll be expected to do very similar promotional
|
||||||
|
work that you would do if you were self published.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="self-publishing-overview" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">How self publishing works</h2>
|
||||||
|
<p>
|
||||||
|
Self publishing removes the agent and publisher from the process. Instead,
|
||||||
|
the author takes responsibility for preparing the book and distributing it
|
||||||
|
through platforms such as Amazon KDP, IngramSpark, or audiobook services
|
||||||
|
like ACX.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The author controls the process from start to finish. That includes editing,
|
||||||
|
cover design, formatting, pricing, and marketing.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
While this offers far greater control, it also means the author must learn
|
||||||
|
skills traditionally handled by publishing professionals.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="self-publishing-pros" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Advantages of self publishing</h2>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Full creative control</li>
|
||||||
|
<li>Faster publishing timelines</li>
|
||||||
|
<li>Higher royalty percentages</li>
|
||||||
|
<li>Ability to publish longer or unconventional works</li>
|
||||||
|
<li>Direct relationship with readers</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-3 mb-0">
|
||||||
|
For authors who enjoy the creative and technical side of publishing,
|
||||||
|
self publishing can be extremely empowering.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="self-publishing-cons" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Challenges of self publishing</h2>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>The author must manage the entire publishing process</li>
|
||||||
|
<li>Up-front costs such as covers, editing, or ISBNs</li>
|
||||||
|
<li>Marketing responsibility falls largely on the author</li>
|
||||||
|
<li>Discoverability can be difficult for new writers</li>
|
||||||
|
<li>Bookshops may be less likely to stock independently published titles</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-3 mb-0">
|
||||||
|
Self publishing offers freedom, but it also requires a willingness to learn
|
||||||
|
and manage many different aspects of the publishing process.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mt-3 mb-0">
|
||||||
|
Everything involved in self publishing is paid for by the author. Nothing is
|
||||||
|
for free.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="genre-and-length-expectations" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Genre expectations and word counts</h2>
|
||||||
|
<p>
|
||||||
|
Traditional publishing often expects manuscripts to fit within specific
|
||||||
|
genre categories and typical word-count ranges. For example, some genres
|
||||||
|
have unofficial expectations around the length of a debut novel.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Self publishing removes most of these constraints. Authors can experiment
|
||||||
|
more freely with structure, length, and storytelling style.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
That freedom can be creatively liberating, but it also means the author must
|
||||||
|
decide what works best for the story rather than relying on industry norms.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="control-vs-support" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">Control vs support</h2>
|
||||||
|
<p>
|
||||||
|
One of the clearest differences between the two approaches is the balance
|
||||||
|
between control and support.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Traditional publishing provides professional teams who help shape the book
|
||||||
|
and bring it to market. Self publishing places that responsibility directly
|
||||||
|
in the hands of the author.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
Some writers prefer the guidance and structure of traditional publishing,
|
||||||
|
while others value the independence and flexibility of doing it themselves.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="my-view" class="mb-5">
|
||||||
|
<h2 class="h3 fw-bold mb-3">My personal view</h2>
|
||||||
|
<p>
|
||||||
|
Both routes have strengths. Traditional publishing offers professional
|
||||||
|
support and established distribution channels. Self publishing offers
|
||||||
|
independence and creative freedom.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For many authors the decision comes down to what they want from the experience.
|
||||||
|
Some writers prefer to focus entirely on writing and leave the rest to a
|
||||||
|
publishing team. Others enjoy being involved in every stage of bringing
|
||||||
|
a book to life.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Neither path is guaranteed to lead to success. Both require persistence,
|
||||||
|
patience, and a willingness to keep improving with every book.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For me, the decision was an easy one. My writing doesn't fall into a neat genre.
|
||||||
|
Its gritty and raw and there simply isn't one that fits. In addition I like to write
|
||||||
|
character rich stories where the characters develop and grow with the reader.
|
||||||
|
</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
I feel
|
||||||
|
that if I were to try and go down the traditional publishing route editors would hate
|
||||||
|
my work. They'd want to reduce it considerably. They love limiting an author's work
|
||||||
|
to 80,000 words whcih I could never allow. I also feel they would want to rip the
|
||||||
|
heart out of my work, reducing the characterisation to just a few lines, making
|
||||||
|
descriptive passages one paragraph maximum. Sorry, but that's not for me. I reckognise
|
||||||
|
that emersive novels aren't for everyone, and that's fine. But they are what I like to
|
||||||
|
write.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="sticky-lg-top" style="top: 2rem;">
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">On this page</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2"><a href="#traditional-publishing-overview" class="text-decoration-none">Traditional publishing</a></li>
|
||||||
|
<li class="mb-2"><a href="#traditional-pros" class="text-decoration-none">Advantages</a></li>
|
||||||
|
<li class="mb-2"><a href="#traditional-cons" class="text-decoration-none">Challenges</a></li>
|
||||||
|
<li class="mb-2"><a href="#self-publishing-overview" class="text-decoration-none">Self publishing</a></li>
|
||||||
|
<li class="mb-2"><a href="#self-publishing-pros" class="text-decoration-none">Advantages</a></li>
|
||||||
|
<li class="mb-2"><a href="#self-publishing-cons" class="text-decoration-none">Challenges</a></li>
|
||||||
|
<li class="mb-2"><a href="#genre-and-length-expectations" class="text-decoration-none">Genre expectations</a></li>
|
||||||
|
<li class="mb-2"><a href="#control-vs-support" class="text-decoration-none">Control vs support</a></li>
|
||||||
|
<li><a href="#my-view" class="text-decoration-none">My view</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Quick takeaway</h2>
|
||||||
|
<p class="small mb-0">
|
||||||
|
Traditional publishing offers support and distribution. Self publishing
|
||||||
|
offers control and flexibility. The right choice depends on the author’s
|
||||||
|
goals and working style.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-0 bg-white shadow-sm rounded-4">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="h5 fw-bold mb-3">Related guides</h2>
|
||||||
|
<ul class="list-unstyled small mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="ReviewsAndArcReaders" class="text-decoration-none">
|
||||||
|
Reviews and ARC Readers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="KdpVsIngramSpark" class="text-decoration-none">
|
||||||
|
KDP vs IngramSpark
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index" class="text-decoration-none">
|
||||||
|
Guide Index
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top pt-4 mt-5">
|
||||||
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Index" class="btn btn-dark">
|
||||||
|
Back to guide index
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a asp-controller="IndieAuthor" asp-action="Isbns" class="btn btn-dark">
|
||||||
|
Next: ISBNs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
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>
|
||||||
113
CatherineLynwood/Views/Reckoning/Extras.cshtml
Normal file
113
CatherineLynwood/Views/Reckoning/Extras.cshtml
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "The Alpha Flame: Reckoning 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="Reckoning" asp-action="Index">Reckoning</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">The Alpha Flame Reckoning Exclusive Extras</h1>
|
||||||
|
<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-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-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-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-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-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>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
264
CatherineLynwood/Views/Reckoning/Index.cshtml
Normal file
264
CatherineLynwood/Views/Reckoning/Index.cshtml
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "The Alpha Flame: Reckoning";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="reckoning-page-shell">
|
||||||
|
<!-- Detective desk / case file section -->
|
||||||
|
<section class="reckoning-desk-section">
|
||||||
|
<div class="reckoning-desk-surface">
|
||||||
|
|
||||||
|
<div class="reckoning-desk-props">
|
||||||
|
<div class="reckoning-desk-photos">
|
||||||
|
<div class="reckoning-desk-photo reckoning-desk-photo-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="reckoning-desk-photo reckoning-desk-photo-2">
|
||||||
|
<responsive-image src="sophie-jones-nightclub.png" class="img-fluid" alt="Sophie Jones" display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="reckoning-desk-photo reckoning-desk-photo-3">
|
||||||
|
<responsive-image src="the-alpha-flame-reckoning-cover.png" class="img-fluid" alt="Maggie Grant" display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-desk-postit">
|
||||||
|
<span>Fashion show<br />Scandals,<br />tonight!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-casefile-page">
|
||||||
|
<div class="casefile-stage">
|
||||||
|
|
||||||
|
<div class="casefile" id="caseFile">
|
||||||
|
|
||||||
|
<div class="casefile-rear">
|
||||||
|
<img src="/images/webp/folder-rear.webp" alt="Case file interior" class="casefile-rear-img" />
|
||||||
|
<div class="casefile-flap-shadow"></div>
|
||||||
|
|
||||||
|
<div class="casefile-inner">
|
||||||
|
<div class="casefile-paper-stack">
|
||||||
|
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-1"></div>
|
||||||
|
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-2"></div>
|
||||||
|
|
||||||
|
<div class="casefile-content">
|
||||||
|
<div class="casefile-panel active" data-tab="summary">
|
||||||
|
<partial name="_ReckoningSummary" model="Model"></partial>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="subjects">
|
||||||
|
<partial name="_ReckoningSubjects" model="Model"></partial>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="evidence">
|
||||||
|
<partial name="_ReckoningEvidence" model="Model"></partial>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="notes">
|
||||||
|
<partial name="_ReckoningReview" model="Model"></partial>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="buy">
|
||||||
|
<partial name="_ReckoningBuy" model="Model"></partial>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel restricted-panel" data-tab="restricted">
|
||||||
|
<partial name="_ReckoningRestricted" model="Model"></partial>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-tabs">
|
||||||
|
<button type="button" class="casefile-tab active" data-tab="summary">Summary</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="subjects">Subjects</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="evidence">Evidence</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="notes">Review</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="buy">Purchase</button>
|
||||||
|
<button type="button" class="casefile-tab restricted" data-tab="restricted">Restricted</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-front" id="caseFileFront" role="button" tabindex="0" aria-label="Open case file">
|
||||||
|
<div class="casefile-front-face casefile-front-face-front">
|
||||||
|
<img src="/images/webp/folder-front.webp" alt="Case file cover" class="casefile-front-img" />
|
||||||
|
|
||||||
|
<div class="casefile-front-overlay">
|
||||||
|
<div class="casefile-stamp">
|
||||||
|
RESTRICTED
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-cover-text">
|
||||||
|
<div class="casefile-cover-kicker">WEST MIDLANDS POLICE</div>
|
||||||
|
<h1>ALPHA FLAME: RECKONING</h1>
|
||||||
|
<p>Case Ref: AFR-1983-06</p>
|
||||||
|
<p>Status: Active Investigation</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-open-hint">Click to open file</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-front-face casefile-front-face-back">
|
||||||
|
<img src="/images/webp/folder-front-inside.webp" alt="" class="casefile-front-img casefile-front-img-back" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section BackgroundVideo {
|
||||||
|
<div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@section CSS {
|
||||||
|
<link href="~/css/reckoning.css" rel="stylesheet" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Shadows+Into+Light&display=swap" rel="stylesheet">
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const trigger = document.querySelector(".casefile-audio-trigger");
|
||||||
|
const audio = document.getElementById("evidenceTapeAudio");
|
||||||
|
|
||||||
|
if (!trigger || !audio) return;
|
||||||
|
|
||||||
|
trigger.addEventListener("click", function () {
|
||||||
|
if (audio.paused) {
|
||||||
|
audio.play().then(function () {
|
||||||
|
trigger.classList.add("is-playing");
|
||||||
|
}).catch(function () {
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
audio.pause();
|
||||||
|
trigger.classList.remove("is-playing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("pause", function () {
|
||||||
|
if (!audio.ended) {
|
||||||
|
trigger.classList.remove("is-playing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("ended", function () {
|
||||||
|
trigger.classList.remove("is-playing");
|
||||||
|
audio.currentTime = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const caseFile = document.getElementById("caseFile");
|
||||||
|
const frontCover = document.getElementById("caseFileFront");
|
||||||
|
const tabs = caseFile.querySelectorAll(".casefile-tab");
|
||||||
|
const panels = caseFile.querySelectorAll(".casefile-panel");
|
||||||
|
const images = caseFile.querySelectorAll("img");
|
||||||
|
|
||||||
|
let loadedCount = 0;
|
||||||
|
let assetsReady = false;
|
||||||
|
let openTimer = null;
|
||||||
|
let isOpening = false;
|
||||||
|
let hasEnteredView = false;
|
||||||
|
|
||||||
|
function openCaseFile() {
|
||||||
|
if (!assetsReady || isOpening || caseFile.classList.contains("open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpening = true;
|
||||||
|
caseFile.classList.add("revealed");
|
||||||
|
caseFile.classList.add("open");
|
||||||
|
|
||||||
|
if (openTimer) {
|
||||||
|
clearTimeout(openTimer);
|
||||||
|
openTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoOpenTimer() {
|
||||||
|
if (!assetsReady || hasEnteredView === false || openTimer || caseFile.classList.contains("open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openTimer = setTimeout(() => {
|
||||||
|
openCaseFile();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAssetLoaded() {
|
||||||
|
loadedCount++;
|
||||||
|
|
||||||
|
if (loadedCount === images.length) {
|
||||||
|
assetsReady = true;
|
||||||
|
caseFile.classList.add("assets-ready");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
caseFile.classList.add("ready");
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
startAutoOpenTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !hasEnteredView) {
|
||||||
|
hasEnteredView = true;
|
||||||
|
startAutoOpenTimer();
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
threshold: 0.35
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(caseFile);
|
||||||
|
|
||||||
|
images.forEach(img => {
|
||||||
|
if (img.complete) {
|
||||||
|
onAssetLoaded();
|
||||||
|
} else {
|
||||||
|
img.addEventListener("load", onAssetLoaded, { once: true });
|
||||||
|
img.addEventListener("error", onAssetLoaded, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frontCover.addEventListener("click", function () {
|
||||||
|
openCaseFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
frontCover.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
openCaseFile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener("click", function () {
|
||||||
|
const tabName = this.dataset.tab;
|
||||||
|
|
||||||
|
tabs.forEach(t => t.classList.remove("active"));
|
||||||
|
panels.forEach(p => p.classList.remove("active"));
|
||||||
|
|
||||||
|
this.classList.add("active");
|
||||||
|
|
||||||
|
const targetPanel = caseFile.querySelector(`.casefile-panel[data-tab="${tabName}"]`);
|
||||||
|
if (targetPanel) {
|
||||||
|
targetPanel.classList.add("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
143
CatherineLynwood/Views/Reckoning/IndexMobile.cshtml
Normal file
143
CatherineLynwood/Views/Reckoning/IndexMobile.cshtml
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "The Alpha Flame: Reckoning";
|
||||||
|
bool showReviews = Model.Reviews.Items.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="reckoning-mobile-page">
|
||||||
|
|
||||||
|
<header class="reckoning-mobile-hero">
|
||||||
|
<div class="reckoning-mobile-hero-inner">
|
||||||
|
<div class="reckoning-mobile-stamp">RESTRICTED</div>
|
||||||
|
<div class="reckoning-mobile-kicker">WEST MIDLANDS POLICE</div>
|
||||||
|
<h1>The Alpha Flame: Reckoning</h1>
|
||||||
|
<p class="reckoning-mobile-ref">Case Ref: AFR-1983-06</p>
|
||||||
|
<p class="reckoning-mobile-status">Status: Active Investigation</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="reckoning-mobile-props">
|
||||||
|
<div class="reckoning-mobile-props-scroll">
|
||||||
|
<div class="reckoning-mobile-polaroid polaroid-tilt-left">
|
||||||
|
<responsive-image src="the-alpha-flame-reckoning-cover.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="The Alpha Flame: Reckoning book cover"
|
||||||
|
display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="reckoning-mobile-polaroid polaroid-tilt-right">
|
||||||
|
<responsive-image src="sophie-jones-nightclub.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="Sophie Jones"
|
||||||
|
display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-mobile-polaroid polaroid-tilt-slight">
|
||||||
|
<responsive-image src="flyover-at-night.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="The Rubery flyover at night time"
|
||||||
|
display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="reckoning-mobile-nav">
|
||||||
|
<a href="#summary">Summary</a>
|
||||||
|
<a href="#subjects">Subjects</a>
|
||||||
|
<a href="#evidence">Evidence</a>
|
||||||
|
<a href="#review">Review</a>
|
||||||
|
<a href="#purchase">Buy</a>
|
||||||
|
<a href="#restricted">Restricted</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="reckoning-mobile-file">
|
||||||
|
|
||||||
|
<section id="summary" class="reckoning-mobile-section">
|
||||||
|
<div class="reckoning-mobile-tab reckoning-tab-summary">Summary</div>
|
||||||
|
<div class="reckoning-mobile-paper">
|
||||||
|
@await Html.PartialAsync("_ReckoningSummary", Model)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="subjects" class="reckoning-mobile-section">
|
||||||
|
<div class="reckoning-mobile-tab reckoning-tab-subjects">Subjects</div>
|
||||||
|
<div class="reckoning-mobile-paper">
|
||||||
|
@await Html.PartialAsync("_ReckoningSubjects", Model)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="evidence" class="reckoning-mobile-section">
|
||||||
|
<div class="reckoning-mobile-tab reckoning-tab-evidence">Evidence</div>
|
||||||
|
<div class="reckoning-mobile-paper">
|
||||||
|
@await Html.PartialAsync("_ReckoningEvidence", Model)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="review" class="reckoning-mobile-section">
|
||||||
|
<div class="reckoning-mobile-tab reckoning-tab-review">Review</div>
|
||||||
|
<div class="reckoning-mobile-paper">
|
||||||
|
@await Html.PartialAsync("_ReckoningReview", Model)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="purchase" class="reckoning-mobile-section">
|
||||||
|
<div class="reckoning-mobile-tab reckoning-tab-purchase">Purchase</div>
|
||||||
|
<div class="reckoning-mobile-paper">
|
||||||
|
@await Html.PartialAsync("_ReckoningBuy", Model)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="restricted" class="reckoning-mobile-section">
|
||||||
|
<div class="reckoning-mobile-tab reckoning-tab-restricted">Restricted</div>
|
||||||
|
<div class="reckoning-mobile-paper">
|
||||||
|
@await Html.PartialAsync("_ReckoningRestricted", Model)
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section BackgroundVideo {
|
||||||
|
<div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@section CSS {
|
||||||
|
<link href="~/css/reckoning-mobile.css" rel="stylesheet" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Shadows+Into+Light&display=swap" rel="stylesheet">
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const trigger = document.querySelector(".casefile-audio-trigger");
|
||||||
|
const audio = document.getElementById("evidenceTapeAudio");
|
||||||
|
|
||||||
|
if (!trigger || !audio) return;
|
||||||
|
|
||||||
|
trigger.addEventListener("click", function () {
|
||||||
|
if (audio.paused) {
|
||||||
|
audio.play().then(function () {
|
||||||
|
trigger.classList.add("is-playing");
|
||||||
|
}).catch(function () {
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
audio.pause();
|
||||||
|
trigger.classList.remove("is-playing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("pause", function () {
|
||||||
|
if (!audio.ended) {
|
||||||
|
trigger.classList.remove("is-playing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("ended", function () {
|
||||||
|
trigger.classList.remove("is-playing");
|
||||||
|
audio.currentTime = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
375
CatherineLynwood/Views/Reckoning/Index_2.cshtml
Normal file
375
CatherineLynwood/Views/Reckoning/Index_2.cshtml
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
@*
|
||||||
|
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||||
|
*@
|
||||||
|
@{
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="reckoning-page-shell">
|
||||||
|
|
||||||
|
<!-- Hero / standard book layout -->
|
||||||
|
<section class="reckoning-hero-section">
|
||||||
|
<div class="reckoning-hero-video">
|
||||||
|
<video id="siteBackgroundVideo"
|
||||||
|
class="reckoning-hero-video-element"
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsinline
|
||||||
|
preload="none">
|
||||||
|
</video>
|
||||||
|
<div class="reckoning-hero-video-overlay"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="container-xxl">
|
||||||
|
<div class="row reckoning-hero-row align-items-stretch">
|
||||||
|
|
||||||
|
<div class="col-lg-5 col-xl-4 d-flex reckoning-hero-cover-column">
|
||||||
|
<div class="reckoning-hero-cover-panel">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-7 col-xl-8 d-flex reckoning-hero-info-column">
|
||||||
|
<div class="reckoning-hero-info-panel">
|
||||||
|
<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">
|
||||||
|
Some secrets stay buried. Others demand a reckoning.
|
||||||
|
</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>
|
||||||
|
<p>
|
||||||
|
As the truth about their mother begins to surface, Maggie and Beth are drawn deeper into a web of lies,
|
||||||
|
hidden money, and dangerous men who would do anything to keep old secrets buried. The closer they get to the
|
||||||
|
truth, the clearer it becomes that someone has been watching them all along.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
And this time, walking away isn’t an option.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Call to action row -->
|
||||||
|
<div class="d-flex gap-3 flex-wrap">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detective desk / case file section -->
|
||||||
|
<section class="reckoning-desk-section">
|
||||||
|
<div class="reckoning-desk-surface">
|
||||||
|
|
||||||
|
<div class="reckoning-desk-props">
|
||||||
|
<div class="reckoning-desk-photos">
|
||||||
|
<div class="reckoning-desk-photo reckoning-desk-photo-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="reckoning-desk-photo reckoning-desk-photo-2">
|
||||||
|
<responsive-image src="sophie-jones-nightclub.png" class="img-fluid" alt="Sophie Jones" display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="reckoning-desk-photo reckoning-desk-photo-3">
|
||||||
|
<responsive-image src="the-alpha-flame-reckoning-cover.png" class="img-fluid" alt="Maggie Grant" display-width-percentage="50"></responsive-image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-desk-postit">
|
||||||
|
<span>Maggie<br />Scandals,<br />tonight!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-casefile-page">
|
||||||
|
<div class="casefile-stage">
|
||||||
|
|
||||||
|
<div class="casefile" id="caseFile">
|
||||||
|
|
||||||
|
<div class="casefile-rear">
|
||||||
|
<img src="/images/webp/folder-rear.webp" alt="Case file interior" class="casefile-rear-img" />
|
||||||
|
<div class="casefile-flap-shadow"></div>
|
||||||
|
|
||||||
|
<div class="casefile-inner">
|
||||||
|
<div class="casefile-paper-stack">
|
||||||
|
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-1"></div>
|
||||||
|
<div class="casefile-paper-stack-layer casefile-paper-stack-layer-2"></div>
|
||||||
|
|
||||||
|
<div class="casefile-content">
|
||||||
|
<div class="casefile-panel active" data-tab="summary">
|
||||||
|
<h2>Case Summary</h2>
|
||||||
|
<p>
|
||||||
|
They thought they were searching for the past. They didn’t realise the past was searching for them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
As Maggie and Beth begin to piece together the fragments of their lives, the line between truth and danger begins to blur. Every answer leads to another question. Every step forward pulls them deeper into something neither of them fully understands.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What began as a search for identity has uncovered a pattern… one that stretches back years, touching lives that were never meant to be connected.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Witnesses are unreliable. Records are incomplete. And certain names keep appearing where they shouldn’t.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Someone has been watching.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
And now, they’re getting closer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="subjects">
|
||||||
|
<h2>Subjects</h2>
|
||||||
|
<p class="reckoning-case-subtitle">Primary persons connected to ongoing enquiries.</p>
|
||||||
|
|
||||||
|
<div class="reckoning-subjects-scroll">
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<h3>SUBJECT: MAGGIE</h3>
|
||||||
|
<p><strong>Age:</strong> Early 20s</p>
|
||||||
|
<p><strong>Status:</strong> Civilian</p>
|
||||||
|
<p><strong>Notes:</strong> Confident, observant, and increasingly proactive in ongoing events. Demonstrates strong protective instincts toward Beth and shows a marked tendency to pursue answers independently. Appears stronger than initial assessments suggested. Extent of involvement remains unclear.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<h3>SUBJECT: BETH</h3>
|
||||||
|
<p><strong>Age:</strong> Early 20s</p>
|
||||||
|
<p><strong>Status:</strong> Victim / Person of Interest</p>
|
||||||
|
<p><strong>Notes:</strong> Known to have suffered sustained trauma and coercive control. Holds key links to several individuals under observation. Emotional condition fragile, though recent behaviour suggests increasing resilience. May possess critical information, whether knowingly or otherwise.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<h3>PERSON OF INTEREST: SIMON JONES</h3>
|
||||||
|
<p><strong>Occupation:</strong> Business owner</p>
|
||||||
|
<p><strong>Status:</strong> Under informal scrutiny</p>
|
||||||
|
<p><strong>Notes:</strong> Name appears repeatedly across multiple lines of enquiry. Public profile respectable, though several associations remain unresolved. No direct action taken to date. Influence believed to extend further than officially recorded.</p>
|
||||||
|
<p class="reckoning-redacted">Known associates: ███████████████</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<h3>PERSON OF INTEREST: SOPHIE JONES</h3>
|
||||||
|
<p><strong>Age:</strong> Early 20s</p>
|
||||||
|
<p><strong>Status:</strong> Associated party</p>
|
||||||
|
<p><strong>Notes:</strong> Outwardly composed and cooperative, but behaviour raises concern. Presence noted around several incidents without formal connection being established. Considered potentially significant. Further attention advised.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="evidence">
|
||||||
|
<h2>Evidence</h2>
|
||||||
|
<p>Recovered fragments. Missing pieces. Unanswered questions.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel" data-tab="notes">
|
||||||
|
<h2>File Notes</h2>
|
||||||
|
<p>Edition details, formats, release notes, and purchase information.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-panel restricted-panel" data-tab="restricted">
|
||||||
|
<h2>Restricted</h2>
|
||||||
|
<p class="restricted-label">Authorised Access Only</p>
|
||||||
|
<p>Bonus archive material is locked.</p>
|
||||||
|
<a href="#buy-now" class="restricted-button">Unlock Access</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-tabs">
|
||||||
|
<button type="button" class="casefile-tab active" data-tab="summary">Summary</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="subjects">Subjects</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="evidence">Evidence</button>
|
||||||
|
<button type="button" class="casefile-tab" data-tab="notes">File Notes</button>
|
||||||
|
<button type="button" class="casefile-tab restricted" data-tab="restricted">Restricted</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-front" id="caseFileFront" role="button" tabindex="0" aria-label="Open case file">
|
||||||
|
<div class="casefile-front-face casefile-front-face-front">
|
||||||
|
<img src="/images/webp/folder-front.webp" alt="Case file cover" class="casefile-front-img" />
|
||||||
|
|
||||||
|
<div class="casefile-front-overlay">
|
||||||
|
<div class="casefile-stamp">
|
||||||
|
RESTRICTED
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-cover-text">
|
||||||
|
<div class="casefile-cover-kicker">WEST MIDLANDS POLICE</div>
|
||||||
|
<h1>ALPHA FLAME: RECKONING</h1>
|
||||||
|
<p>Case Ref: AFR-1983-06</p>
|
||||||
|
<p>Status: Active Investigation</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-open-hint">Click to open file</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-front-face casefile-front-face-back">
|
||||||
|
<img src="/images/webp/folder-front-inside.webp" alt="" class="casefile-front-img casefile-front-img-back" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section BackgroundVideo {
|
||||||
|
<div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@section CSS {
|
||||||
|
<link href="~/css/reckoning.css" rel="stylesheet" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const caseFile = document.getElementById("caseFile");
|
||||||
|
const frontCover = document.getElementById("caseFileFront");
|
||||||
|
const tabs = caseFile.querySelectorAll(".casefile-tab");
|
||||||
|
const panels = caseFile.querySelectorAll(".casefile-panel");
|
||||||
|
const images = caseFile.querySelectorAll("img");
|
||||||
|
|
||||||
|
let loadedCount = 0;
|
||||||
|
let assetsReady = false;
|
||||||
|
let openTimer = null;
|
||||||
|
let isOpening = false;
|
||||||
|
let hasEnteredView = false;
|
||||||
|
|
||||||
|
function openCaseFile() {
|
||||||
|
if (!assetsReady || isOpening || caseFile.classList.contains("open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpening = true;
|
||||||
|
caseFile.classList.add("revealed");
|
||||||
|
caseFile.classList.add("open");
|
||||||
|
|
||||||
|
if (openTimer) {
|
||||||
|
clearTimeout(openTimer);
|
||||||
|
openTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoOpenTimer() {
|
||||||
|
if (!assetsReady || hasEnteredView === false || openTimer || caseFile.classList.contains("open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openTimer = setTimeout(() => {
|
||||||
|
openCaseFile();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAssetLoaded() {
|
||||||
|
loadedCount++;
|
||||||
|
|
||||||
|
if (loadedCount === images.length) {
|
||||||
|
assetsReady = true;
|
||||||
|
caseFile.classList.add("assets-ready");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
caseFile.classList.add("ready");
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
startAutoOpenTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !hasEnteredView) {
|
||||||
|
hasEnteredView = true;
|
||||||
|
startAutoOpenTimer();
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
threshold: 0.35
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(caseFile);
|
||||||
|
|
||||||
|
images.forEach(img => {
|
||||||
|
if (img.complete) {
|
||||||
|
onAssetLoaded();
|
||||||
|
} else {
|
||||||
|
img.addEventListener("load", onAssetLoaded, { once: true });
|
||||||
|
img.addEventListener("error", onAssetLoaded, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frontCover.addEventListener("click", function () {
|
||||||
|
openCaseFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
frontCover.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
openCaseFile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener("click", function () {
|
||||||
|
const tabName = this.dataset.tab;
|
||||||
|
|
||||||
|
tabs.forEach(t => t.classList.remove("active"));
|
||||||
|
panels.forEach(p => p.classList.remove("active"));
|
||||||
|
|
||||||
|
this.classList.add("active");
|
||||||
|
|
||||||
|
const targetPanel = caseFile.querySelector(`.casefile-panel[data-tab="${tabName}"]`);
|
||||||
|
if (targetPanel) {
|
||||||
|
targetPanel.classList.add("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
287
CatherineLynwood/Views/Reckoning/Index_old.cshtml
Normal file
287
CatherineLynwood/Views/Reckoning/Index_old.cshtml
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
@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>
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
|
||||||
|
|
||||||
|
</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>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
42
CatherineLynwood/Views/Reckoning/_Layout.cshtml
Normal file
42
CatherineLynwood/Views/Reckoning/_Layout.cshtml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@{
|
||||||
|
Layout = "/Views/Shared/_Layout.cshtml";
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Meta {
|
||||||
|
@RenderSection("Meta", required: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@section CSS {
|
||||||
|
@RenderSection("CSS", required: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (IsSectionDefined("BackgroundVideo"))
|
||||||
|
{
|
||||||
|
@section BackgroundVideo {
|
||||||
|
@RenderSection("BackgroundVideo", required: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@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)
|
||||||
|
}
|
||||||
3
CatherineLynwood/Views/Reckoning/_ReckoningBuy.cshtml
Normal file
3
CatherineLynwood/Views/Reckoning/_ReckoningBuy.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
@await Component.InvokeAsync("BuyPanel", new { ISO2 = Model.UserIso2, Src = Model.Src, Title = Model.Title })
|
||||||
105
CatherineLynwood/Views/Reckoning/_ReckoningEvidence.cshtml
Normal file
105
CatherineLynwood/Views/Reckoning/_ReckoningEvidence.cshtml
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
<h2>Evidence</h2>
|
||||||
|
<p class="casefile-evidence-intro">
|
||||||
|
Recovered materials connected to the case. Several items remain incomplete, damaged, or subject to restricted review.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="casefile-evidence-list">
|
||||||
|
|
||||||
|
<section class="casefile-evidence-item casefile-evidence-item-featured">
|
||||||
|
<div class="casefile-evidence-item-header">
|
||||||
|
<div class="casefile-evidence-item-id">Item 01</div>
|
||||||
|
<h3>Audio Recordings</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-evidence-media">
|
||||||
|
<button type="button"
|
||||||
|
class="casefile-audio-trigger"
|
||||||
|
data-audio-target="evidenceTapeAudio"
|
||||||
|
aria-label="Play recovered tape audio">
|
||||||
|
|
||||||
|
<span class="casefile-audio-image-wrapper">
|
||||||
|
<responsive-image src="cassette-tape.png"
|
||||||
|
class="casefile-evidence-image"
|
||||||
|
alt="Cassette tape evidence in plastic case"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
|
||||||
|
<span class="casefile-audio-overlay"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<audio id="evidenceTapeAudio" preload="auto">
|
||||||
|
<source src="/audio/cassette-tape.mp3" type="audio/mpeg" />
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Recovered from secured deposit storage. Handwritten labels inconsistent. Audio quality poor in places.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Fragmented conversation captured between unidentified voices. Repeated references to money, locations, and an unnamed female subject.
|
||||||
|
</p>
|
||||||
|
<p class="casefile-evidence-note">
|
||||||
|
Transcript not included.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="casefile-evidence-item">
|
||||||
|
<div class="casefile-evidence-item-header">
|
||||||
|
<div class="casefile-evidence-item-id">Item 02</div>
|
||||||
|
<h3>Deposit Record Extract</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-evidence-media">
|
||||||
|
<responsive-image src="bank-book.png"
|
||||||
|
class="casefile-evidence-image"
|
||||||
|
alt="Deposit book page containing an encoded number"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Final page of deposit record book. Contains handwritten notation believed to reference a coded box number.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Meaning not immediately apparent. Pattern only becomes visible when cross-referenced with accompanying material.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="casefile-evidence-item">
|
||||||
|
<div class="casefile-evidence-item-header">
|
||||||
|
<div class="casefile-evidence-item-id">Item 03</div>
|
||||||
|
<h3>Handwritten Note</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-evidence-paperclip">
|
||||||
|
<responsive-image src="blackmail-note.png"
|
||||||
|
class="casefile-evidence-image"
|
||||||
|
alt="Partially folded handwritten note"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Small folded note recovered during follow-up enquiries. Believed to have been used to direct movement between locations.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Full wording withheld from this file. Relevance considered significant.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="casefile-evidence-item">
|
||||||
|
<div class="casefile-evidence-item-header">
|
||||||
|
<div class="casefile-evidence-item-id">Item 04</div>
|
||||||
|
<h3>Redacted Document Extract</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="casefile-redacted-block">
|
||||||
|
<p>██████████████████████████████████████</p>
|
||||||
|
<p>██████████████████ ███████████████████</p>
|
||||||
|
<p>Reference to individual removed from file.</p>
|
||||||
|
<p>Access restricted pending review.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
12
CatherineLynwood/Views/Reckoning/_ReckoningRestricted.cshtml
Normal file
12
CatherineLynwood/Views/Reckoning/_ReckoningRestricted.cshtml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
<h2>Restricted</h2>
|
||||||
|
<p class="restricted-label">Authorised Access Only</p>
|
||||||
|
<p>Bonus archive material is locked.</p>
|
||||||
|
<p>
|
||||||
|
When you've purchased an edition of The Alpha Flame: Reckoning come back to gain access to all sort of hidden extras.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
New material is being added all the time.
|
||||||
|
</p>
|
||||||
|
<a asp-action="Extras" class="restricted-button">Unlock Access</a>
|
||||||
89
CatherineLynwood/Views/Reckoning/_ReckoningReview.cshtml
Normal file
89
CatherineLynwood/Views/Reckoning/_ReckoningReview.cshtml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
@{
|
||||||
|
bool showReviews = Model.Reviews.Items.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>External Review</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Supplementary observations and external responses filed in relation to this case.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Public reaction remains limited but notable. The following extract has been selected for inclusion.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@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="casefile-reviews">
|
||||||
|
<h3 class="casefile-section-heading">External Commentary</h3>
|
||||||
|
|
||||||
|
<div class="casefile-review-entry">
|
||||||
|
<div class="casefile-review-stars" aria-label="@top.RatingValue out of 5 stars">
|
||||||
|
@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 casefile-review-star-empty"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<blockquote class="casefile-review-body">
|
||||||
|
@Html.Raw(top.ReviewBody)
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<p class="casefile-review-source">
|
||||||
|
<strong>@top.AuthorName</strong> on
|
||||||
|
@if (string.IsNullOrEmpty(top.URL))
|
||||||
|
{
|
||||||
|
<span>@top.SiteName</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@top.URL" target="_blank" rel="noopener noreferrer">@top.SiteName</a>
|
||||||
|
}
|
||||||
|
<span class="casefile-review-date">, @reviewDate</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Model.Reviews.Items.Count > 1)
|
||||||
|
{
|
||||||
|
<div class="casefile-review-more">
|
||||||
|
<a asp-action="Reviews" class="casefile-inline-link">Read more reviews</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<section class="casefile-reviews">
|
||||||
|
<h3 class="casefile-section-heading">External Commentary</h3>
|
||||||
|
<p>
|
||||||
|
No reader responses have yet been added to this file.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<p class="casefile-notes-meta">
|
||||||
|
<strong>Classification:</strong> Circulation ongoing<br />
|
||||||
|
<strong>Reference:</strong> AF-R/02
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="casefile-notes-margin">
|
||||||
|
<em>Margin note:</em> “Keep watching this one.”
|
||||||
|
</p>
|
||||||
80
CatherineLynwood/Views/Reckoning/_ReckoningSubjects.cshtml
Normal file
80
CatherineLynwood/Views/Reckoning/_ReckoningSubjects.cshtml
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
<h2>Subjects</h2>
|
||||||
|
<p class="reckoning-case-subtitle">Primary persons connected to ongoing enquiries.</p>
|
||||||
|
|
||||||
|
<div class="reckoning-subjects-scroll">
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<responsive-image src="maggie-grant-36.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="Maggie Grant"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>SUBJECT: GRANT, MAGGIE</h3>
|
||||||
|
<p><strong>AKA:</strong> Fletcher, Grace</p>
|
||||||
|
<p><strong>DoB:</strong> 01/09/1965</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p><strong>Status:</strong> Victim</p>
|
||||||
|
<p><strong>Notes:</strong> Confident, observant, and increasingly proactive in ongoing events. Demonstrates strong protective instincts toward Beth and shows a marked tendency to pursue answers independently. Appears stronger than initial assessments suggested. Extent of involvement remains unclear.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<responsive-image src="beth-6.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="Beth Fletcher"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>SUBJECT: FLETCHER, BETH</h3>
|
||||||
|
<p><strong>DoB:</strong> 31/08/1965</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p><strong>Status:</strong> Victim</p>
|
||||||
|
<p><strong>Notes:</strong> Known to have suffered sustained trauma and coercive control. Holds key links to several individuals under observation. Emotional condition fragile, though recent behaviour suggests increasing resilience. May possess critical information, whether knowingly or otherwise.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<responsive-image src="simon-jones-mug-shot.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="Simon Jones"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>PERSON OF INTEREST: JONES, SIMON</h3>
|
||||||
|
<p><strong>DoB:</strong> 15/010/1935</p>
|
||||||
|
<p><strong>Occupation:</strong> Business owner</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Status:</strong> Under informal scrutiny</p>
|
||||||
|
<p><strong>Notes:</strong> Name appears repeatedly across multiple lines of enquiry. Public profile respectable, though several associations remain unresolved. No direct action taken to date. Influence believed to extend further than officially recorded.</p>
|
||||||
|
<p class="reckoning-redacted">Known associates: ███████████████</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="reckoning-subject-entry">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<responsive-image src="sophie-jones-street-shot.png"
|
||||||
|
class="img-polaroid"
|
||||||
|
alt="Sophie Jones"
|
||||||
|
display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>PERSON OF INTEREST: JONES, SOPHIE</h3>
|
||||||
|
<p><strong>DoB:</strong> 07/03/1962</p>
|
||||||
|
<p><strong>Status:</strong> Under investigation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Notes:</strong> Outwardly composed and cooperative, but behaviour raises concern. Presence noted around several incidents without formal connection being established. Considered potentially significant. Further attention advised.</p>
|
||||||
|
<p class="reckoning-redacted">Known associates: Harris, Richard Arthur</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
21
CatherineLynwood/Views/Reckoning/_ReckoningSummary.cshtml
Normal file
21
CatherineLynwood/Views/Reckoning/_ReckoningSummary.cshtml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@model CatherineLynwood.Models.TitlePageViewModel
|
||||||
|
|
||||||
|
<h2>Case Summary</h2>
|
||||||
|
<p>
|
||||||
|
They thought they were searching for the past. They didn’t realise the past was searching for them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
As Maggie and Beth begin to piece together the fragments of their lives, the line between truth and danger begins to blur. Every answer leads to another question. Every step forward pulls them deeper into something neither of them fully understands.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
What began as a search for identity has uncovered a pattern… one that stretches back years, touching lives that were never meant to be connected.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Witnesses are unreliable. Records are incomplete. And certain names keep appearing where they shouldn’t.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Someone has been watching.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
And now, they’re getting closer.
|
||||||
|
</p>
|
||||||
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