Save
This commit is contained in:
parent
27cfdd8f6d
commit
0a2f596628
@ -154,10 +154,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||||
|
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||||
<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.2.1" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<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.8" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
|
||||||
|
|||||||
@ -1,51 +1,71 @@
|
|||||||
using CatherineLynwood.Controllers;
|
using CatherineLynwood.Models;
|
||||||
using CatherineLynwood.Models;
|
|
||||||
using CatherineLynwood.Services;
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
namespace CatherineLynwood.Controllers
|
||||||
|
|
||||||
public class AccessController : Controller
|
|
||||||
{
|
{
|
||||||
#region Private Fields
|
[Route("access")]
|
||||||
|
public class AccessController : Controller
|
||||||
private readonly IAccessCodeService _accessCodeService;
|
|
||||||
private readonly ILogger<HomeController> _logger;
|
|
||||||
private DataAccess _dataAccess;
|
|
||||||
|
|
||||||
#endregion Private Fields
|
|
||||||
|
|
||||||
#region Public Constructors
|
|
||||||
|
|
||||||
public AccessController(ILogger<HomeController> logger, DataAccess dataAccess, IAccessCodeService accessCodeService)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
#region Private Fields
|
||||||
_dataAccess = dataAccess;
|
|
||||||
_accessCodeService = accessCodeService;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Public Constructors
|
private readonly IAccessCodeService _accessCodeService;
|
||||||
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
private DataAccess _dataAccess;
|
||||||
|
|
||||||
#region Public Methods
|
#endregion Private Fields
|
||||||
|
|
||||||
[HttpGet]
|
#region Public Constructors
|
||||||
public IActionResult MailingList()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
public AccessController(ILogger<HomeController> logger, DataAccess dataAccess, IAccessCodeService accessCodeService)
|
||||||
public async Task<IActionResult> MailingList(Marketing marketing)
|
|
||||||
{
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(marketing.Email))
|
|
||||||
{
|
{
|
||||||
success = await _dataAccess.AddMarketingAsync(marketing);
|
_logger = logger;
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
_accessCodeService = accessCodeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success)
|
#endregion Public Constructors
|
||||||
|
|
||||||
|
#region Public Methods
|
||||||
|
|
||||||
|
[HttpGet("mailinglist")]
|
||||||
|
public IActionResult MailingList()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("mailinglist")]
|
||||||
|
public async Task<IActionResult> MailingList(Marketing marketing)
|
||||||
|
{
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(marketing.Email))
|
||||||
|
{
|
||||||
|
success = await _dataAccess.AddMarketingAsync(marketing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
// Set cookie so we don’t ask again
|
||||||
|
Response.Cookies.Append("MailingListPrompted", "true", new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddYears(5),
|
||||||
|
IsEssential = true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect back to original destination
|
||||||
|
var returnUrl = TempData["ReturnUrl"] as string ?? "/extras";
|
||||||
|
return Redirect(returnUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return RedirectToAction("Prompt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("decline")]
|
||||||
|
public IActionResult Decline()
|
||||||
{
|
{
|
||||||
// Set cookie so we don’t ask again
|
// Set cookie so we don’t ask again
|
||||||
Response.Cookies.Append("MailingListPrompted", "true", new CookieOptions
|
Response.Cookies.Append("MailingListPrompted", "true", new CookieOptions
|
||||||
@ -55,70 +75,52 @@ public class AccessController : Controller
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Redirect back to original destination
|
// Redirect back to original destination
|
||||||
var returnUrl = TempData["ReturnUrl"] as string ?? "/Extras";
|
var returnUrl = TempData["ReturnUrl"] as string ?? "/extras";
|
||||||
return Redirect(returnUrl);
|
return Redirect(returnUrl);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
[HttpGet("prompt")]
|
||||||
|
public async Task<IActionResult> Prompt(string returnUrl = "/extras")
|
||||||
{
|
{
|
||||||
return RedirectToAction("Prompt");
|
var (pageNumber, wordIndex) = await _accessCodeService.GetCurrentChallengeAsync();
|
||||||
|
|
||||||
|
ViewBag.PageNumber = pageNumber;
|
||||||
|
ViewBag.WordIndex = wordIndex;
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
|
||||||
|
return View();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost("prompt")]
|
||||||
public IActionResult Decline()
|
public async Task<IActionResult> Prompt(string userWord, string returnUrl = "/extras")
|
||||||
{
|
|
||||||
// Set cookie so we don’t ask again
|
|
||||||
Response.Cookies.Append("MailingListPrompted", "true", new CookieOptions
|
|
||||||
{
|
{
|
||||||
Expires = DateTimeOffset.UtcNow.AddYears(5),
|
var (accessLevel, highestBook) = await _accessCodeService.ValidateWordAsync(userWord);
|
||||||
IsEssential = true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redirect back to original destination
|
if (accessLevel > 0 && highestBook > 0)
|
||||||
var returnUrl = TempData["ReturnUrl"] as string ?? "/Extras";
|
|
||||||
return Redirect(returnUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> Prompt(string returnUrl = "/Extras")
|
|
||||||
{
|
|
||||||
var (pageNumber, wordIndex) = await _accessCodeService.GetCurrentChallengeAsync();
|
|
||||||
|
|
||||||
ViewBag.PageNumber = pageNumber;
|
|
||||||
ViewBag.WordIndex = wordIndex;
|
|
||||||
ViewBag.ReturnUrl = returnUrl;
|
|
||||||
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Prompt(string userWord, string returnUrl = "/Extras")
|
|
||||||
{
|
|
||||||
var (accessLevel, highestBook) = await _accessCodeService.ValidateWordAsync(userWord);
|
|
||||||
|
|
||||||
if (accessLevel > 0 && highestBook > 0)
|
|
||||||
{
|
|
||||||
HttpContext.Session.SetInt32("BookAccessLevel", accessLevel);
|
|
||||||
HttpContext.Session.SetInt32("BookAccessMax", highestBook);
|
|
||||||
|
|
||||||
var promptedCookie = Request.Cookies["MailingListPrompted"];
|
|
||||||
if (string.IsNullOrEmpty(promptedCookie))
|
|
||||||
{
|
{
|
||||||
TempData["ReturnUrl"] = returnUrl;
|
HttpContext.Session.SetInt32("BookAccessLevel", accessLevel);
|
||||||
return RedirectToAction("MailingList");
|
HttpContext.Session.SetInt32("BookAccessMax", highestBook);
|
||||||
|
|
||||||
|
var promptedCookie = Request.Cookies["MailingListPrompted"];
|
||||||
|
if (string.IsNullOrEmpty(promptedCookie))
|
||||||
|
{
|
||||||
|
TempData["ReturnUrl"] = returnUrl;
|
||||||
|
return RedirectToAction("MailingList");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Redirect(returnUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Redirect(returnUrl);
|
var (pageNumber, wordIndex) = await _accessCodeService.GetCurrentChallengeAsync();
|
||||||
|
ViewBag.PageNumber = pageNumber;
|
||||||
|
ViewBag.WordIndex = wordIndex;
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
ViewBag.Error = "Invalid word. Please try again.";
|
||||||
|
|
||||||
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (pageNumber, wordIndex) = await _accessCodeService.GetCurrentChallengeAsync();
|
#endregion Public Methods
|
||||||
ViewBag.PageNumber = pageNumber;
|
|
||||||
ViewBag.WordIndex = wordIndex;
|
|
||||||
ViewBag.ReturnUrl = returnUrl;
|
|
||||||
ViewBag.Error = "Invalid word. Please try again.";
|
|
||||||
|
|
||||||
return View();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Methods
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
CatherineLynwood/Controllers/AccountController.cs
Normal file
51
CatherineLynwood/Controllers/AccountController.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Controllers
|
||||||
|
{
|
||||||
|
public class AccountController : Controller
|
||||||
|
{
|
||||||
|
private const string HardcodedUsername = "nick";
|
||||||
|
private const string HardcodedPassword = "ryaN9982?"; // Change this
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Login(string returnUrl = "/")
|
||||||
|
{
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Login(string username, string password, string returnUrl = "/")
|
||||||
|
{
|
||||||
|
if (username == HardcodedUsername && password == HardcodedPassword)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Name, username)
|
||||||
|
};
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, "MyCookieAuth");
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
|
await HttpContext.SignInAsync("MyCookieAuth", principal);
|
||||||
|
|
||||||
|
return LocalRedirect(returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewBag.Error = "Invalid credentials";
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync("MyCookieAuth");
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
CatherineLynwood/Controllers/AdminController.cs
Normal file
142
CatherineLynwood/Controllers/AdminController.cs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Controllers
|
||||||
|
{
|
||||||
|
[Route("admin")]
|
||||||
|
[Authorize]
|
||||||
|
public class AdminController : Controller
|
||||||
|
{
|
||||||
|
#region Private Fields
|
||||||
|
|
||||||
|
private readonly IWebHostEnvironment _env;
|
||||||
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
private DataAccess _dataAccess;
|
||||||
|
|
||||||
|
#endregion Private Fields
|
||||||
|
|
||||||
|
#region Public Constructors
|
||||||
|
|
||||||
|
public AdminController(IWebHostEnvironment env, ILogger<HomeController> logger, DataAccess dataAccess)
|
||||||
|
{
|
||||||
|
_env = env;
|
||||||
|
_logger = logger;
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Public Constructors
|
||||||
|
|
||||||
|
[Route("")]
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("arc-readers")]
|
||||||
|
public async Task<IActionResult> ArcReaders()
|
||||||
|
{
|
||||||
|
ARCReaderList aRCReaderList = await _dataAccess.GetAllARCReadersAsync();
|
||||||
|
|
||||||
|
return View(aRCReaderList);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("blog")]
|
||||||
|
public async Task<IActionResult> Blog()
|
||||||
|
{
|
||||||
|
BlogAdminIndex blogAdminIndex = await _dataAccess.GetBlogAdminIndexAsync();
|
||||||
|
|
||||||
|
return View(blogAdminIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("blog/edit/{slug}")]
|
||||||
|
public async Task<IActionResult> BlogEdit(string slug)
|
||||||
|
{
|
||||||
|
Blog blog = await _dataAccess.GetBlogItemAsync(slug);
|
||||||
|
|
||||||
|
ViewBag.ResponderList = await _dataAccess.GetResponderList();
|
||||||
|
|
||||||
|
return View(blog);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("blog/edit/{slug}")]
|
||||||
|
public async Task<IActionResult> Edit(string slug, BlogEditPage page)
|
||||||
|
{
|
||||||
|
var root = _env.WebRootPath;
|
||||||
|
var imageRoot = Path.Combine(root, "images");
|
||||||
|
var audioRoot = Path.Combine(root, "audio");
|
||||||
|
var videoRoot = Path.Combine(root, "videos");
|
||||||
|
|
||||||
|
// Ensure directories exist
|
||||||
|
Directory.CreateDirectory(imageRoot);
|
||||||
|
Directory.CreateDirectory(audioRoot);
|
||||||
|
Directory.CreateDirectory(videoRoot);
|
||||||
|
|
||||||
|
// For Image (.png)
|
||||||
|
if (page.ImageUpload != null && page.ImageUpload.Length > 0)
|
||||||
|
{
|
||||||
|
page.ImageUrl = page.BlogUrl + ".png";
|
||||||
|
var imagePath = Path.Combine(imageRoot, page.ImageUrl);
|
||||||
|
using (var stream = new FileStream(imagePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await page.ImageUpload.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Audio Teaser (.mp3)
|
||||||
|
if (page.AudioTeaserUpload != null && page.AudioTeaserUpload.Length > 0)
|
||||||
|
{
|
||||||
|
page.AudioTeaserUrl = page.BlogUrl + "_teaser.mp3";
|
||||||
|
var audioTeaserPath = Path.Combine(audioRoot, page.AudioTeaserUrl);
|
||||||
|
using (var stream = new FileStream(audioTeaserPath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await page.AudioTeaserUpload.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Audio Transcript (.mp3)
|
||||||
|
if (page.AudioTranscriptUpload != null && page.AudioTranscriptUpload.Length > 0)
|
||||||
|
{
|
||||||
|
page.AudioTranscriptUrl = page.BlogUrl + "_transcript.mp3";
|
||||||
|
var audioTranscriptPath = Path.Combine(audioRoot, page.AudioTranscriptUrl);
|
||||||
|
using (var stream = new FileStream(audioTranscriptPath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await page.AudioTranscriptUpload.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Video (.mp4)
|
||||||
|
if (page.VideoUpload != null && page.VideoUpload.Length > 0)
|
||||||
|
{
|
||||||
|
page.VideoUrl = page.BlogUrl + ".mp4";
|
||||||
|
var videoPath = Path.Combine(videoRoot, page.VideoUrl);
|
||||||
|
using (var stream = new FileStream(videoPath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await page.VideoUpload.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = await _dataAccess.UpdateBlogAsync(page);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return RedirectToAction("Blog");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, "An unknown error occured.");
|
||||||
|
|
||||||
|
Blog blog = await _dataAccess.GetBlogItemAsync(slug);
|
||||||
|
|
||||||
|
ViewBag.ResponderList = await _dataAccess.GetResponderList();
|
||||||
|
|
||||||
|
return View(blog);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,9 @@
|
|||||||
using CatherineLynwood.Helpers;
|
using CatherineLynwood.Models;
|
||||||
using CatherineLynwood.Models;
|
|
||||||
using CatherineLynwood.Services;
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
using SendGrid.Helpers.Mail;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace CatherineLynwood.Controllers
|
namespace CatherineLynwood.Controllers
|
||||||
{
|
{
|
||||||
@ -13,10 +11,12 @@ namespace CatherineLynwood.Controllers
|
|||||||
public class AskAQuestion : Controller
|
public class AskAQuestion : Controller
|
||||||
{
|
{
|
||||||
private DataAccess _dataAccess;
|
private DataAccess _dataAccess;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
|
|
||||||
public AskAQuestion(DataAccess dataAccess)
|
public AskAQuestion(DataAccess dataAccess, IEmailService emailService)
|
||||||
{
|
{
|
||||||
_dataAccess = dataAccess;
|
_dataAccess = dataAccess;
|
||||||
|
_emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index(bool showThanks)
|
public async Task<IActionResult> Index(bool showThanks)
|
||||||
@ -48,7 +48,6 @@ namespace CatherineLynwood.Controllers
|
|||||||
|
|
||||||
if (!visible)
|
if (!visible)
|
||||||
{
|
{
|
||||||
var to = new EmailAddress(question.EmailAddress, question.Name);
|
|
||||||
var subject = "Thank you from Catherine Lynwood Web Site";
|
var subject = "Thank you from Catherine Lynwood Web Site";
|
||||||
var plainTextContent = $"Dear {question.Name},/r/nThank you for taking the time to ask me a question. ";
|
var plainTextContent = $"Dear {question.Name},/r/nThank you for taking the time to ask me a question. ";
|
||||||
var htmlContent = $"Dear {question.Name},<br>" +
|
var htmlContent = $"Dear {question.Name},<br>" +
|
||||||
@ -60,7 +59,14 @@ namespace CatherineLynwood.Controllers
|
|||||||
$"<p>Catherine Lynwood<br>" +
|
$"<p>Catherine Lynwood<br>" +
|
||||||
$"Author: The Alpha Flame<br>" +
|
$"Author: The Alpha Flame<br>" +
|
||||||
@$"Web: <a href=""https://www.catherinelynwood.com"">www.catherinelynwood.com</a></p>";
|
@$"Web: <a href=""https://www.catherinelynwood.com"">www.catherinelynwood.com</a></p>";
|
||||||
await SendEmail.Execute(to, subject, plainTextContent, htmlContent);
|
|
||||||
|
Contact contact = new Contact
|
||||||
|
{
|
||||||
|
Name = question.Name,
|
||||||
|
EmailAddress = question.EmailAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
showThanks = true;
|
showThanks = true;
|
||||||
|
|||||||
86
CatherineLynwood/Controllers/BlogController.cs
Normal file
86
CatherineLynwood/Controllers/BlogController.cs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class BlogController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IWebHostEnvironment _env;
|
||||||
|
private readonly DataAccess _dataAccess;
|
||||||
|
private readonly string _blogApiKey;
|
||||||
|
|
||||||
|
public BlogController(
|
||||||
|
IWebHostEnvironment env,
|
||||||
|
DataAccess dataAccess,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_env = env;
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
_blogApiKey = configuration["ApiKeys:BlogPost"];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateBlog([FromBody] BlogPostRequest blog)
|
||||||
|
{
|
||||||
|
// 1️⃣ Check for API key in headers
|
||||||
|
//if (!Request.Headers.TryGetValue("X-API-Key", out var apiKey) || apiKey != _blogApiKey)
|
||||||
|
//{
|
||||||
|
// return StatusCode(StatusCodes.Status403Forbidden, "Invalid or missing API key.");
|
||||||
|
//}
|
||||||
|
|
||||||
|
// 2️⃣ Validate input
|
||||||
|
if (blog == null || string.IsNullOrWhiteSpace(blog.BlogUrl))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid blog data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call your DB save method
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dataAccess.SaveBlogToDatabase(blog);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, $"Error saving to database: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new { status = "success", message = "Blog post saved successfully." });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAllBlogs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var blogs = await _dataAccess.GetAllBlogsAsync();
|
||||||
|
|
||||||
|
var summaries = blogs.Select(b => new BlogSummaryResponse
|
||||||
|
{
|
||||||
|
BlogUrl = b.BlogUrl,
|
||||||
|
Title = b.Title,
|
||||||
|
SubTitle = b.SubTitle,
|
||||||
|
IndexText = b.IndexText,
|
||||||
|
AiSummary = b.AiSummary,
|
||||||
|
PublishDate = b.PublishDate
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(summaries);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, $"Error retrieving blogs: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,33 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using CatherineLynwood.Models;
|
||||||
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace CatherineLynwood.Controllers
|
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 DataAccess _dataAccess;
|
||||||
|
|
||||||
|
#endregion Private Fields
|
||||||
|
|
||||||
|
#region Public Constructors
|
||||||
|
|
||||||
|
public DiscoveryController(DataAccess dataAccess)
|
||||||
|
{
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Public Constructors
|
||||||
|
|
||||||
#region Public Methods
|
#region Public Methods
|
||||||
|
|
||||||
[Route("chapters/chapter-1-beth")]
|
[Route("chapters/chapter-1-beth")]
|
||||||
@ -40,9 +63,21 @@ namespace CatherineLynwood.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Route("")]
|
[Route("")]
|
||||||
public IActionResult Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
return View();
|
Reviews reviews = await _dataAccess.GetReviewsAsync();
|
||||||
|
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 3);
|
||||||
|
|
||||||
|
return View(reviews);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("reviews")]
|
||||||
|
public async Task<IActionResult> Reviews()
|
||||||
|
{
|
||||||
|
Reviews reviews = await _dataAccess.GetReviewsAsync();
|
||||||
|
reviews.SchemaJsonLd = GenerateBookSchemaJsonLd(reviews, 100);
|
||||||
|
|
||||||
|
return View(reviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BookAccess(1, 1)]
|
[BookAccess(1, 1)]
|
||||||
@ -67,5 +102,152 @@ namespace CatherineLynwood.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Methods
|
#endregion Public Methods
|
||||||
|
|
||||||
|
#region Private Methods
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add review section if there are reviews
|
||||||
|
if (reviews?.Items?.Any() == true)
|
||||||
|
{
|
||||||
|
var reviewObjects = new List<Dictionary<string, object>>();
|
||||||
|
double total = 0;
|
||||||
|
|
||||||
|
foreach (var review in reviews.Items.Take(take))
|
||||||
|
{
|
||||||
|
total += review.RatingValue;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add work examples
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
string StripHtml(string input) => string.IsNullOrWhiteSpace(input) ? string.Empty : Regex.Replace(input, "<.*?>", string.Empty);
|
||||||
|
|
||||||
|
|
||||||
|
#endregion Private Methods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,11 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
|
|
||||||
using CatherineLynwood.Helpers;
|
|
||||||
using CatherineLynwood.Models;
|
using CatherineLynwood.Models;
|
||||||
using CatherineLynwood.Services;
|
using CatherineLynwood.Services;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
using SendGrid.Helpers.Mail;
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace CatherineLynwood.Controllers
|
namespace CatherineLynwood.Controllers
|
||||||
{
|
{
|
||||||
@ -18,25 +15,42 @@ namespace CatherineLynwood.Controllers
|
|||||||
|
|
||||||
private readonly ILogger<HomeController> _logger;
|
private readonly ILogger<HomeController> _logger;
|
||||||
private DataAccess _dataAccess;
|
private DataAccess _dataAccess;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
|
|
||||||
#endregion Private Fields
|
#endregion Private Fields
|
||||||
|
|
||||||
#region Public Constructors
|
#region Public Constructors
|
||||||
|
|
||||||
public HomeController(ILogger<HomeController> logger, DataAccess dataAccess)
|
public HomeController(ILogger<HomeController> logger, DataAccess dataAccess, IEmailService emailService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dataAccess = dataAccess;
|
_dataAccess = dataAccess;
|
||||||
|
_emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Constructors
|
#endregion Public Constructors
|
||||||
|
|
||||||
#region Public Methods
|
#region Public Methods
|
||||||
|
|
||||||
[Route("collaboration-inquiry")]
|
[HttpGet("arc-reader-application")]
|
||||||
public IActionResult Honeypot()
|
public IActionResult ArcReaderApplication()
|
||||||
{
|
{
|
||||||
return View();
|
ArcReaderApplicationModel arcReaderApplicationModel = new ArcReaderApplicationModel();
|
||||||
|
|
||||||
|
return View(arcReaderApplicationModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("arc-reader-application")]
|
||||||
|
public async Task<IActionResult> ArcReaderApplication(ArcReaderApplicationModel arcReaderApplicationModel)
|
||||||
|
{
|
||||||
|
bool success = await _dataAccess.SaveARCReaderApplication(arcReaderApplicationModel);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return RedirectToAction("ThankYou");
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(arcReaderApplicationModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("about-catherine-lynwood")]
|
[Route("about-catherine-lynwood")]
|
||||||
@ -62,11 +76,13 @@ namespace CatherineLynwood.Controllers
|
|||||||
{
|
{
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var to = new EmailAddress("catherine@catherinelynwood.com", "Catherine Lynwood");
|
await _dataAccess.SaveContact(contact);
|
||||||
|
|
||||||
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}";
|
||||||
var htmlContent = $"<strong>Email from: {contact.Name} ({contact.EmailAddress})</strong><br>{contact.Message}";
|
var htmlContent = $"<strong>Email from: {contact.Name} ({contact.EmailAddress})</strong><br>{contact.Message}";
|
||||||
await SendEmail.Execute(to, subject, plainTextContent, htmlContent);
|
|
||||||
|
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact);
|
||||||
|
|
||||||
return RedirectToAction("ThankYou");
|
return RedirectToAction("ThankYou");
|
||||||
}
|
}
|
||||||
@ -80,7 +96,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
[Route("feed")]
|
[Route("feed")]
|
||||||
public async Task<IActionResult> DownloadRssFeed()
|
public async Task<IActionResult> DownloadRssFeed()
|
||||||
{
|
{
|
||||||
BlogIndex blogIndex = await _dataAccess.GetBlogsAsync(string.Empty);
|
BlogIndex blogIndex = await _dataAccess.GetBlogsAsync();
|
||||||
|
|
||||||
XNamespace atom = "http://www.w3.org/2005/Atom"; // Define the Atom namespace
|
XNamespace atom = "http://www.w3.org/2005/Atom"; // Define the Atom namespace
|
||||||
|
|
||||||
@ -139,6 +155,12 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Route("collaboration-inquiry")]
|
||||||
|
public IActionResult Honeypot()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
//await SendEmail.Execute();
|
//await SendEmail.Execute();
|
||||||
@ -146,13 +168,8 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("samantha-lynwood")]
|
[Route("offline")]
|
||||||
public IActionResult SamanthaLynwood()
|
public IActionResult Offline()
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IActionResult ThankYou()
|
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
@ -163,8 +180,20 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("offline")]
|
[Route("samantha-lynwood")]
|
||||||
public IActionResult Offline()
|
public IActionResult SamanthaLynwood()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("thankyou")]
|
||||||
|
public IActionResult ThankYou()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("verostic-genre")]
|
||||||
|
public IActionResult VerosticGenre()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
{
|
{
|
||||||
public class PublishingController : Controller
|
public class PublishingController : Controller
|
||||||
{
|
{
|
||||||
|
[Route("publishing")]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
|
|||||||
@ -32,20 +32,24 @@ namespace CatherineLynwood.Controllers
|
|||||||
{
|
{
|
||||||
var urls = new List<SitemapEntry>
|
var urls = new List<SitemapEntry>
|
||||||
{
|
{
|
||||||
new SitemapEntry { Url = Url.Action("Index", "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("AboutCatherineLynwood", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
new SitemapEntry { Url = Url.Action("AboutCatherineLynwood", "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("ArcReaderApplication", "Home", 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", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
|
||||||
new SitemapEntry { Url = Url.Action("Blog", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
new SitemapEntry { Url = Url.Action("Blog", "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("Index", "Discovery", 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("Privacy", "Home", 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("Chapter1", "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("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("Reviews", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Characters", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("ContactCatherine", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Giveaways", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Index", "AskAQuestion", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Index", "Discovery", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Index", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Index", "Publishing", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Index", "TheAlphaFlame", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("Privacy", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("SamanthaLynwood", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
|
new SitemapEntry { Url = Url.Action("VerosticGenre", "Home", null, Request.Scheme).TrimEnd('/'), LastModified = DateTime.UtcNow },
|
||||||
// Additional static pages
|
// Additional static pages
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,7 +65,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add blog URLs dynamically, with PublishDate as LastModified
|
// Add blog URLs dynamically, with PublishDate as LastModified
|
||||||
BlogIndex blogIndex = await _dataAccess.GetBlogsAsync(null);
|
BlogIndex blogIndex = await _dataAccess.GetBlogsAsync();
|
||||||
foreach (var post in blogIndex.Blogs)
|
foreach (var post in blogIndex.Blogs)
|
||||||
{
|
{
|
||||||
urls.Add(new SitemapEntry
|
urls.Add(new SitemapEntry
|
||||||
@ -98,7 +102,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
xmlWriter.WriteStartElement("url");
|
xmlWriter.WriteStartElement("url");
|
||||||
xmlWriter.WriteElementString("loc", entry.Url);
|
xmlWriter.WriteElementString("loc", entry.Url);
|
||||||
xmlWriter.WriteElementString("lastmod", entry.LastModified.ToString("yyyy-MM-dd"));
|
xmlWriter.WriteElementString("lastmod", entry.LastModified.ToString("yyyy-MM-dd"));
|
||||||
xmlWriter.WriteElementString("changefreq", "weekly");
|
xmlWriter.WriteElementString("changefreq", "daily");
|
||||||
xmlWriter.WriteElementString("priority", "0.5");
|
xmlWriter.WriteElementString("priority", "0.5");
|
||||||
xmlWriter.WriteEndElement();
|
xmlWriter.WriteEndElement();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ using Microsoft.Identity.Client;
|
|||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
using SendGrid.Helpers.Mail;
|
|
||||||
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace CatherineLynwood.Controllers
|
namespace CatherineLynwood.Controllers
|
||||||
@ -19,14 +17,16 @@ namespace CatherineLynwood.Controllers
|
|||||||
#region Private Fields
|
#region Private Fields
|
||||||
|
|
||||||
private DataAccess _dataAccess;
|
private DataAccess _dataAccess;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
|
|
||||||
#endregion Private Fields
|
#endregion Private Fields
|
||||||
|
|
||||||
#region Public Constructors
|
#region Public Constructors
|
||||||
|
|
||||||
public TheAlphaFlameController(DataAccess dataAccess)
|
public TheAlphaFlameController(DataAccess dataAccess, IEmailService emailService)
|
||||||
{
|
{
|
||||||
_dataAccess = dataAccess;
|
_dataAccess = dataAccess;
|
||||||
|
_emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Constructors
|
#endregion Public Constructors
|
||||||
@ -40,72 +40,54 @@ namespace CatherineLynwood.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Route("blog")]
|
[Route("blog")]
|
||||||
public async Task<IActionResult> Blog(BlogFilter blogFilter)
|
[Route("blog/{page:int}")]
|
||||||
|
public async Task<IActionResult> Blog(BlogFilter blogFilter, int page)
|
||||||
{
|
{
|
||||||
// Convert the Categories list to a comma-separated string for querying
|
blogFilter.PageNumber = page == 0 ? 1 : page;
|
||||||
string categoryIDs = blogFilter.Categories != null ? string.Join(",", blogFilter.Categories) : string.Empty;
|
|
||||||
|
|
||||||
// Retrieve the blogs filtered by categories
|
// Set up your logic as before, including sorting, filtering, and pagination...
|
||||||
BlogIndex blogIndex = await _dataAccess.GetBlogsAsync(categoryIDs);
|
|
||||||
|
BlogIndex blogIndex = await _dataAccess.GetBlogsAsync();
|
||||||
blogIndex.BlogFilter = blogFilter;
|
blogIndex.BlogFilter = blogFilter;
|
||||||
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);
|
||||||
|
|
||||||
if (blogFilter.TotalPages != blogFilter.PreviousTotalPages)
|
// Apply sorting
|
||||||
{
|
|
||||||
blogFilter.PageNumber = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine sorting direction: 1 = newest first, 2 = oldest first
|
|
||||||
if (blogFilter.SortDirection == 1)
|
if (blogFilter.SortDirection == 1)
|
||||||
{
|
|
||||||
// Sort by newest first
|
|
||||||
blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList();
|
blogIndex.Blogs = blogIndex.Blogs.OrderByDescending(b => b.PublishDate).ToList();
|
||||||
}
|
|
||||||
else if (blogFilter.SortDirection == 2)
|
else if (blogFilter.SortDirection == 2)
|
||||||
{
|
|
||||||
// Sort by oldest first
|
|
||||||
blogIndex.Blogs = blogIndex.Blogs.OrderBy(b => b.PublishDate).ToList();
|
blogIndex.Blogs = blogIndex.Blogs.OrderBy(b => b.PublishDate).ToList();
|
||||||
}
|
|
||||||
|
|
||||||
// Paginate the results based on ResultsPerPage
|
// Paginate
|
||||||
int resultsPerPage = blogFilter.ResultsPerPage > 0 ? blogFilter.ResultsPerPage : 6; // Default to 6 if not specified
|
int resultsPerPage = blogFilter.ResultsPerPage > 0 ? blogFilter.ResultsPerPage : 6;
|
||||||
|
|
||||||
// Calculate the items for the current page
|
|
||||||
blogIndex.Blogs = blogIndex.Blogs
|
blogIndex.Blogs = blogIndex.Blogs
|
||||||
.Skip((blogFilter.PageNumber - 1) * resultsPerPage)
|
.Skip((blogFilter.PageNumber - 1) * resultsPerPage)
|
||||||
.Take(resultsPerPage)
|
.Take(resultsPerPage)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Show advanced options if any category filters are applied
|
|
||||||
blogIndex.ShowAdvanced = !string.IsNullOrEmpty(categoryIDs);
|
|
||||||
|
|
||||||
return View(blogIndex);
|
return View(blogIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Route("blog/{slug}")]
|
[Route("blog/{slug}")]
|
||||||
public async Task<IActionResult> BlogItem(string slug, bool showThanks)
|
public async Task<IActionResult> BlogItem(string slug, bool showThanks, int? pageNumber, int? sortDirection, int? resultsPerPage)
|
||||||
{
|
{
|
||||||
Blog blog = await _dataAccess.GetBlogItemAsync(slug);
|
var blog = await _dataAccess.GetBlogItemAsync(slug);
|
||||||
|
|
||||||
if (blog.Title == null)
|
if (blog?.Title == null)
|
||||||
{
|
|
||||||
return RedirectPermanent("/the-alpha-flame/blog");
|
return RedirectPermanent("/the-alpha-flame/blog");
|
||||||
}
|
|
||||||
|
|
||||||
blog.ShowThanks = showThanks;
|
blog.ShowThanks = showThanks;
|
||||||
|
|
||||||
// Generate JSON-LD
|
|
||||||
blog.SchemaJsonLd = GenerateBlogSchemaJsonLd(blog);
|
blog.SchemaJsonLd = GenerateBlogSchemaJsonLd(blog);
|
||||||
|
|
||||||
if (blog.Template == "slideshow")
|
// Construct return link
|
||||||
{
|
string pagePart = (pageNumber.HasValue && pageNumber > 1) ? $"/{pageNumber}" : "";
|
||||||
return View("SlideShowTemplate", blog);
|
string queryPart = $"?SortDirection={sortDirection ?? 1}&ResultsPerPage={resultsPerPage ?? 6}";
|
||||||
}
|
ViewBag.BlogReturnLink = $"/the-alpha-flame/blog{pagePart}{queryPart}";
|
||||||
|
|
||||||
return View("DefaultTemplate", blog);
|
return View(blog.Template == "slideshow" ? "SlideShowTemplate" : "DefaultTemplate", blog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Route("characters")]
|
[Route("characters")]
|
||||||
public IActionResult Characters()
|
public IActionResult Characters()
|
||||||
{
|
{
|
||||||
@ -123,7 +105,6 @@ namespace CatherineLynwood.Controllers
|
|||||||
|
|
||||||
if (!visible)
|
if (!visible)
|
||||||
{
|
{
|
||||||
var to = new EmailAddress(blogComment.EmailAddress, blogComment.Name);
|
|
||||||
var subject = "Thank you from Catherine Lynwood Web Site";
|
var subject = "Thank you from Catherine Lynwood Web Site";
|
||||||
var plainTextContent = $"Dear {blogComment.Name},/r/nThank you for taking the time to comment on my blog post. ";
|
var plainTextContent = $"Dear {blogComment.Name},/r/nThank you for taking the time to comment on my blog post. ";
|
||||||
var htmlContent = $"Dear {blogComment.Name},<br>" +
|
var htmlContent = $"Dear {blogComment.Name},<br>" +
|
||||||
@ -135,7 +116,14 @@ namespace CatherineLynwood.Controllers
|
|||||||
$"<p>Catherine Lynwood<br>" +
|
$"<p>Catherine Lynwood<br>" +
|
||||||
$"Author: The Alpha Flame<br>" +
|
$"Author: The Alpha Flame<br>" +
|
||||||
@$"Web: <a href=""https://www.catherinelynwood.com"">www.catherinelynwood.com</a></p>";
|
@$"Web: <a href=""https://www.catherinelynwood.com"">www.catherinelynwood.com</a></p>";
|
||||||
await SendEmail.Execute(to, subject, plainTextContent, htmlContent);
|
|
||||||
|
Contact contact = new Contact
|
||||||
|
{
|
||||||
|
Name = blogComment.Name,
|
||||||
|
EmailAddress = blogComment.EmailAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
await _emailService.SendEmailAsync(subject, plainTextContent, htmlContent, contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
showThanks = true;
|
showThanks = true;
|
||||||
@ -173,6 +161,39 @@ namespace CatherineLynwood.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("giveaways/enter")]
|
||||||
|
public IActionResult Enter()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("giveaways/enter")]
|
||||||
|
public async Task<IActionResult> Enter(Marketing marketing)
|
||||||
|
{
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(marketing.Email))
|
||||||
|
{
|
||||||
|
success = await _dataAccess.EnterGiveawayAsync(marketing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return RedirectToAction("ThankYou");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return RedirectToAction("Enter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("giveaways/thankyou")]
|
||||||
|
public IActionResult Thankyou()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Route("characters/maggie-grant")]
|
[Route("characters/maggie-grant")]
|
||||||
public IActionResult Maggie()
|
public IActionResult Maggie()
|
||||||
{
|
{
|
||||||
@ -239,7 +260,7 @@ namespace CatherineLynwood.Controllers
|
|||||||
|
|
||||||
string contentTopPlainText = StripHtml(blog.ContentTop);
|
string contentTopPlainText = StripHtml(blog.ContentTop);
|
||||||
string contentBottomPlainText = StripHtml(blog.ContentBottom);
|
string contentBottomPlainText = StripHtml(blog.ContentBottom);
|
||||||
string blogUrl = $"https://www.catherinelynwood.com/{blog.BlogUrl}";
|
string blogUrl = $"https://www.catherinelynwood.com/the-alpha-flame/blog/{blog.BlogUrl}";
|
||||||
|
|
||||||
// Build the schema object
|
// Build the schema object
|
||||||
var schema = new Dictionary<string, object>
|
var schema = new Dictionary<string, object>
|
||||||
|
|||||||
34
CatherineLynwood/Helpers/BlogUrlHelper.cs
Normal file
34
CatherineLynwood/Helpers/BlogUrlHelper.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Helpers
|
||||||
|
{
|
||||||
|
public static class BlogUrlHelper
|
||||||
|
{
|
||||||
|
public static string GetPageUrl(int pageNumber, BlogFilter filter)
|
||||||
|
{
|
||||||
|
if (pageNumber <= 1)
|
||||||
|
return "/the-alpha-flame/blog" + ToQueryString(filter, excludePage: true);
|
||||||
|
|
||||||
|
return $"/the-alpha-flame/blog/{pageNumber}" + ToQueryString(filter, excludePage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToQueryString(BlogFilter filter, bool excludePage = false)
|
||||||
|
{
|
||||||
|
var query = HttpUtility.ParseQueryString(string.Empty);
|
||||||
|
|
||||||
|
if (filter.SortDirection != 1)
|
||||||
|
query["SortDirection"] = filter.SortDirection.ToString();
|
||||||
|
|
||||||
|
if (filter.ResultsPerPage != 6)
|
||||||
|
query["ResultsPerPage"] = filter.ResultsPerPage.ToString();
|
||||||
|
|
||||||
|
if (!excludePage && filter.PageNumber > 1)
|
||||||
|
query["PageNumber"] = filter.PageNumber.ToString();
|
||||||
|
|
||||||
|
string queryString = query.ToString();
|
||||||
|
return string.IsNullOrEmpty(queryString) ? "" : "?" + queryString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +0,0 @@
|
|||||||
using SendGrid.Helpers.Mail;
|
|
||||||
using SendGrid;
|
|
||||||
|
|
||||||
namespace CatherineLynwood.Helpers
|
|
||||||
{
|
|
||||||
public class SendEmail
|
|
||||||
{
|
|
||||||
public static async Task Execute(EmailAddress to, string subject, string plainTextContent, string htmlContent)
|
|
||||||
{
|
|
||||||
var apiKey = "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";
|
|
||||||
var client = new SendGridClient(apiKey);
|
|
||||||
var from = new EmailAddress("catherine@catherinelynwood.com", "Catherine Lynwood");
|
|
||||||
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
|
|
||||||
var response = await client.SendEmailAsync(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -21,12 +21,14 @@
|
|||||||
if (host.Equals("catherinelynwood.com", StringComparison.OrdinalIgnoreCase) || schema.Equals("http", StringComparison.OrdinalIgnoreCase))
|
if (host.Equals("catherinelynwood.com", StringComparison.OrdinalIgnoreCase) || schema.Equals("http", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var newUrl = $"https://www.catherinelynwood.com{context.Request.Path}{context.Request.QueryString}";
|
var newUrl = $"https://www.catherinelynwood.com{context.Request.Path}{context.Request.QueryString}";
|
||||||
context.Response.Redirect(newUrl, permanent: true);
|
context.Response.StatusCode = StatusCodes.Status308PermanentRedirect;
|
||||||
return; // End the middleware pipeline.
|
context.Response.Headers["Location"] = newUrl;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Continue to the next middleware.
|
// Continue to the next middleware.
|
||||||
await _next(context);
|
await _next(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,8 @@
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(referer) || !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.OrdinalIgnoreCase)))
|
if (string.IsNullOrEmpty(referer) || !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
context.Response.StatusCode = StatusCodes.Status451UnavailableForLegalReasons;
|
||||||
await context.Response.WriteAsync("Invalid referer.");
|
await context.Response.WriteAsync("Invalid request.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
CatherineLynwood/Middleware/SpamAndSecurityMiddleware.cs
Normal file
153
CatherineLynwood/Middleware/SpamAndSecurityMiddleware.cs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
using Microsoft.Web.Administration;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Middleware
|
||||||
|
{
|
||||||
|
public class SpamAndSecurityMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
private readonly ILogger<SpamAndSecurityMiddleware> _logger;
|
||||||
|
|
||||||
|
// Known bad bots
|
||||||
|
private static readonly string[] BadBots = new[]
|
||||||
|
{
|
||||||
|
"AhrefsBot", "SemrushBot", "MJ12bot", "DotBot", "Baiduspider"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Referer whitelist
|
||||||
|
private static readonly string[] AllowedReferers = new[]
|
||||||
|
{
|
||||||
|
"https://www.catherinelynwood.com",
|
||||||
|
"http://localhost",
|
||||||
|
"https://localhost"
|
||||||
|
};
|
||||||
|
|
||||||
|
public SpamAndSecurityMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IWebHostEnvironment environment,
|
||||||
|
ILogger<SpamAndSecurityMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_environment = environment;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var request = context.Request;
|
||||||
|
var path = request.Path.Value ?? string.Empty;
|
||||||
|
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1️⃣ Enforce canonical HTTPS + www
|
||||||
|
//
|
||||||
|
if (_environment.IsProduction())
|
||||||
|
{
|
||||||
|
var host = request.Host.Host;
|
||||||
|
var scheme = request.Scheme;
|
||||||
|
if (host.Equals("catherinelynwood.com", StringComparison.OrdinalIgnoreCase) || scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var newUrl = $"https://www.catherinelynwood.com{request.Path}{request.QueryString}";
|
||||||
|
_logger.LogInformation("Redirecting to canonical URL: {Url}", newUrl);
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status308PermanentRedirect;
|
||||||
|
context.Response.Headers["Location"] = newUrl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2️⃣ Block .php and .env probes, and optionally ban in IIS
|
||||||
|
//
|
||||||
|
if (path.EndsWith(".php", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".env", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
//if (!_environment.IsDevelopment() && ipAddress != null)
|
||||||
|
//{
|
||||||
|
// TryBlockIpInIIS(ipAddress);
|
||||||
|
//}
|
||||||
|
|
||||||
|
_logger.LogWarning("Blocked .php/.env probe from {IP}: {Path}", ipAddress, path);
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await context.Response.WriteAsync("Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3️⃣ Block known bad bots
|
||||||
|
//
|
||||||
|
var userAgent = request.Headers["User-Agent"].ToString();
|
||||||
|
if (BadBots.Any(bot => userAgent.Contains(bot, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Blocked known bad bot: {UserAgent} from IP {IP}", userAgent, ipAddress);
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await context.Response.WriteAsync("Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 4️⃣ Referer validation on POST
|
||||||
|
//
|
||||||
|
if (request.Method == HttpMethods.Post && !request.Path.StartsWithSegments("/api"))
|
||||||
|
{
|
||||||
|
var referer = request.Headers["Referer"].ToString();
|
||||||
|
|
||||||
|
// ✅ New logic:
|
||||||
|
// Allow if referer is missing/empty
|
||||||
|
// Only block if referer is present but NOT in the whitelist
|
||||||
|
if (!string.IsNullOrEmpty(referer) && !AllowedReferers.Any(r => referer.StartsWith(r, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
//if (!_environment.IsDevelopment() && ipAddress != null)
|
||||||
|
//{
|
||||||
|
// TryBlockIpInIIS(ipAddress);
|
||||||
|
//}
|
||||||
|
|
||||||
|
_logger.LogWarning("Blocked POST with invalid referer: {Referer} from IP {IP}", referer, ipAddress);
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
|
await context.Response.WriteAsync("Invalid request.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// 5️⃣ All checks passed — continue pipeline
|
||||||
|
//
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Helper to add IP restriction in IIS
|
||||||
|
//
|
||||||
|
private void TryBlockIpInIIS(string ipAddress)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var serverManager = new ServerManager();
|
||||||
|
var site = serverManager.Sites["CatherineLynwood"];
|
||||||
|
var config = site.GetWebConfiguration();
|
||||||
|
var ipSecurity = config.GetSection("system.webServer/security/ipSecurity");
|
||||||
|
var collection = ipSecurity.GetCollection();
|
||||||
|
|
||||||
|
var exists = collection.FirstOrDefault(e => e.Attributes["ipAddress"]?.Value?.ToString() == ipAddress);
|
||||||
|
if (exists == null)
|
||||||
|
{
|
||||||
|
var addElement = collection.CreateElement("add");
|
||||||
|
addElement.SetAttributeValue("ipAddress", ipAddress);
|
||||||
|
addElement.SetAttributeValue("allowed", false);
|
||||||
|
collection.Add(addElement);
|
||||||
|
|
||||||
|
serverManager.CommitChanges();
|
||||||
|
_logger.LogInformation("Blocked IP {IP} in IIS", ipAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to block IP in IIS: {IP}", ipAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
CatherineLynwood/Models/ARCReaderApplication.cs
Normal file
53
CatherineLynwood/Models/ARCReaderApplication.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class ARCReaderApplication
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
[Required, StringLength(50)]
|
||||||
|
public string ApprovedSender { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(50)]
|
||||||
|
public string ContentFit { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(200)]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string ExtraNotes { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(100)]
|
||||||
|
public string FullName { get; set; }
|
||||||
|
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(200)]
|
||||||
|
public string KindleEmail { get; set; }
|
||||||
|
|
||||||
|
[StringLength(500)]
|
||||||
|
public string Platforms { get; set; }
|
||||||
|
|
||||||
|
[StringLength(100)]
|
||||||
|
public string PlatformsOther { get; set; }
|
||||||
|
|
||||||
|
[StringLength(500)]
|
||||||
|
public string PreviewChapters { get; set; }
|
||||||
|
|
||||||
|
[Required, StringLength(50)]
|
||||||
|
public string ReviewCommitment { get; set; }
|
||||||
|
|
||||||
|
[StringLength(500)]
|
||||||
|
public string ReviewLink { get; set; }
|
||||||
|
|
||||||
|
// nvarchar(max) — no length limit
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime SubmittedAt { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
|
||||||
|
// matches DEFAULT(getdate())
|
||||||
|
}
|
||||||
|
}
|
||||||
7
CatherineLynwood/Models/ARCReaderList.cs
Normal file
7
CatherineLynwood/Models/ARCReaderList.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class ARCReaderList
|
||||||
|
{
|
||||||
|
public List<ARCReaderApplication> Applications { get; set; } = new List<ARCReaderApplication>();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
CatherineLynwood/Models/ArcReaderApplicationModel.cs
Normal file
54
CatherineLynwood/Models/ArcReaderApplicationModel.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class ArcReaderApplicationModel
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "Please enter your full name.")]
|
||||||
|
[Display(Name = "Your Full Name", Prompt = "e.g. Catherine Lynwood")]
|
||||||
|
[StringLength(100, ErrorMessage = "Name must be under 100 characters.")]
|
||||||
|
public string FullName { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please enter your correspondence email.")]
|
||||||
|
[EmailAddress(ErrorMessage = "Please enter a valid email address.")]
|
||||||
|
[Display(Name = "Correspondence Email", Prompt = "e.g. your@email.com")]
|
||||||
|
[DataType(DataType.EmailAddress)]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please enter your Kindle email address.")]
|
||||||
|
[Display(Name = "Kindle Email Address", Prompt = "e.g. yourname")]
|
||||||
|
public string KindleEmail { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select whether you've approved the sender.")]
|
||||||
|
[Display(Name = "Have you added my sender email to your Approved Senders list?")]
|
||||||
|
public string ApprovedSender { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Where do you plan to post your review?")]
|
||||||
|
public List<string> Platforms { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Have you read / listened to any of the preview chapters on this website?")]
|
||||||
|
public List<string> PreviewChapters { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Other Platform (optional)", Prompt = "e.g. YouTube, Substack")]
|
||||||
|
[StringLength(100)]
|
||||||
|
public string? PlatformsOther { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Link to a Past Review", Prompt = "e.g. https://www.goodreads.com/review/show/...")]
|
||||||
|
[Url(ErrorMessage = "Please enter a valid URL.")]
|
||||||
|
[DataType(DataType.Url)]
|
||||||
|
public string? ReviewLink { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please select whether this book suits your reading preferences.")]
|
||||||
|
[Display(Name = "Do you enjoy verostic fiction?")]
|
||||||
|
public string ContentFit { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Please indicate if you'll be able to leave a review.")]
|
||||||
|
[Display(Name = "Will you aim to leave a review close to the launch date (21st August)?")]
|
||||||
|
public string ReviewCommitment { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Anything else you'd like to share", Prompt = "Tell me how you found the book, or what draws you to this story...")]
|
||||||
|
[DataType(DataType.MultilineText)]
|
||||||
|
[StringLength(1000, ErrorMessage = "Please keep this under 1000 characters.")]
|
||||||
|
public string? ExtraNotes { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,32 @@
|
|||||||
namespace CatherineLynwood.Models
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Models
|
||||||
{
|
{
|
||||||
public class Blog
|
public class Blog : BlogPost
|
||||||
{
|
{
|
||||||
#region Public Properties
|
#region Public Properties
|
||||||
|
|
||||||
public string AudioTeaserText { get; set; }
|
[Display(Name = "AI Summary")]
|
||||||
|
public string? AiSummary { get; set; }
|
||||||
|
|
||||||
public string AudioTeaserUrl { get; set; }
|
[Display(Name = "Audio teaser text")]
|
||||||
|
public string? AudioTeaserText { get; set; }
|
||||||
|
|
||||||
public string AudioTranscriptUrl { get; set; }
|
[Display(Name = "Audio teaser file", Prompt = "Load audio teaser file")]
|
||||||
|
public string? AudioTeaserUrl { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Audio transcript file", Prompt = "Load audio transcript file")]
|
||||||
|
public string? AudioTranscriptUrl { get; set; }
|
||||||
|
|
||||||
public BlogComment BlogComment { get; set; } = new BlogComment();
|
public BlogComment BlogComment { get; set; } = new BlogComment();
|
||||||
|
|
||||||
public int BlogID { get; set; }
|
|
||||||
|
|
||||||
public List<BlogImage> BlogImages { get; set; } = new List<BlogImage>();
|
public List<BlogImage> BlogImages { get; set; } = new List<BlogImage>();
|
||||||
|
|
||||||
public string BlogUrl { get; set; }
|
[Display(Name = "Bottom HTML content")]
|
||||||
|
public string? ContentBottom { get; set; }
|
||||||
|
|
||||||
public string ContentBottom { get; set; }
|
[Display(Name = "Top HTML content")]
|
||||||
|
public string ContentTop { get; set; } = string.Empty;
|
||||||
public string ContentTop { get; set; }
|
|
||||||
|
|
||||||
public string DefaultWebpImage
|
public string DefaultWebpImage
|
||||||
{
|
{
|
||||||
@ -32,35 +38,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ImageAlt { get; set; }
|
[Display(Name = "Is draft")]
|
||||||
|
public bool Draft { get; set; } = true;
|
||||||
|
|
||||||
public string ImageDescription { get; set; }
|
[Display(Name = "Image ALT text")]
|
||||||
|
public string? ImageAlt { get; set; }
|
||||||
|
|
||||||
public bool ImageFirst { get; set; }
|
[Display(Name = "Image caption")]
|
||||||
|
public string? ImageDescription { get; set; }
|
||||||
|
|
||||||
public string ImageUrl { get; set; }
|
[Display(Name = "Image left")]
|
||||||
|
public bool ImageFirst { get; set; } = false;
|
||||||
|
|
||||||
public string IndexText { get; set; }
|
[Display(Name = "Suggested AI image prompt")]
|
||||||
|
public string? ImagePrompt { get; set; }
|
||||||
|
|
||||||
public int Likes { get; set; }
|
[Display(Name = "Image file", Prompt = "Load image file")]
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Has been indexed")]
|
||||||
|
public bool Indexed { get; set; } = false;
|
||||||
|
|
||||||
|
[Display(Name = "Blog index text")]
|
||||||
|
public string IndexText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Display(Name = "Likes")]
|
||||||
|
public int Likes { get; set; } = 0;
|
||||||
|
|
||||||
|
// Default in DB
|
||||||
|
// Nullable with default
|
||||||
|
[Display(Name = "Posted by")]
|
||||||
public string PostedBy { get; set; }
|
public string PostedBy { get; set; }
|
||||||
|
|
||||||
public string PostedImage { get; set; }
|
public string PostedImage { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Publish date")]
|
||||||
public DateTime PublishDate { get; set; }
|
public DateTime PublishDate { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Posted by")]
|
||||||
|
public int ResponderID { get; set; } = 1; // Default constraint in DB
|
||||||
|
|
||||||
public string SchemaJsonLd { get; set; }
|
public string SchemaJsonLd { get; set; }
|
||||||
|
|
||||||
public bool ShowThanks { get; set; }
|
public bool ShowThanks { get; set; }
|
||||||
|
|
||||||
public string SubTitle { get; set; }
|
// Nullable
|
||||||
|
[Display(Name = "Sub title")]
|
||||||
|
public string? SubTitle { get; set; }
|
||||||
|
|
||||||
public string Template { get; set; }
|
[Display(Name = "Template name")]
|
||||||
|
public string Template { get; set; } = "default";
|
||||||
|
|
||||||
public string Title { get; set; }
|
[Display(Name = "Title")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string VideoUrl { get; set; }
|
// Default constraint in DB
|
||||||
|
[Display(Name = "Video file", Prompt = "Load video file")]
|
||||||
|
public string? VideoUrl { get; set; }
|
||||||
|
|
||||||
|
// Default in DB
|
||||||
|
|
||||||
#endregion Public Properties
|
#endregion Public Properties
|
||||||
}
|
}
|
||||||
|
|||||||
7
CatherineLynwood/Models/BlogAdminIndex.cs
Normal file
7
CatherineLynwood/Models/BlogAdminIndex.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class BlogAdminIndex
|
||||||
|
{
|
||||||
|
public List<BlogAdminIndexItem> BlogItems { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
23
CatherineLynwood/Models/BlogAdminIndexItem.cs
Normal file
23
CatherineLynwood/Models/BlogAdminIndexItem.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class BlogAdminIndexItem
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public int BlogID { get; set; }
|
||||||
|
|
||||||
|
public string BlogUrl { get; set; }
|
||||||
|
|
||||||
|
public bool Draft { get; set; }
|
||||||
|
|
||||||
|
public string IndexText { get; set; }
|
||||||
|
|
||||||
|
public DateTime PublishDate { get; set; }
|
||||||
|
|
||||||
|
public string SubTitle { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
13
CatherineLynwood/Models/BlogEditPage.cs
Normal file
13
CatherineLynwood/Models/BlogEditPage.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class BlogEditPage : Blog
|
||||||
|
{
|
||||||
|
public IFormFile ImageUpload { get; set; }
|
||||||
|
|
||||||
|
public IFormFile AudioTranscriptUpload { get; set; }
|
||||||
|
|
||||||
|
public IFormFile AudioTeaserUpload { get; set; }
|
||||||
|
|
||||||
|
public IFormFile VideoUpload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
public int PageNumber { get; set; } = 1;
|
public int PageNumber { get; set; } = 1;
|
||||||
|
|
||||||
public List<int> Categories { get; set; }
|
|
||||||
|
|
||||||
public int TotalPages { get; set; }
|
public int TotalPages { get; set; }
|
||||||
|
|
||||||
public int PreviousTotalPages { get; set; }
|
public int PreviousTotalPages { get; set; }
|
||||||
|
|||||||
@ -4,15 +4,13 @@
|
|||||||
{
|
{
|
||||||
#region Public Properties
|
#region Public Properties
|
||||||
|
|
||||||
public List<BlogCategory> BlogCategories { get; set; } = new List<BlogCategory>();
|
|
||||||
|
|
||||||
public BlogFilter BlogFilter { get; set; }
|
public BlogFilter BlogFilter { get; set; }
|
||||||
|
|
||||||
public List<Blog> Blogs { get; set; } = new List<Blog>();
|
public List<Blog> Blogs { get; set; } = new List<Blog>();
|
||||||
|
|
||||||
public bool IsMobile { get; set; }
|
public Blog NextBlog { get; set; } = new Blog();
|
||||||
|
|
||||||
public bool ShowAdvanced { get; set; }
|
public bool IsMobile { get; set; }
|
||||||
|
|
||||||
#endregion Public Properties
|
#endregion Public Properties
|
||||||
}
|
}
|
||||||
|
|||||||
13
CatherineLynwood/Models/BlogPost.cs
Normal file
13
CatherineLynwood/Models/BlogPost.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class BlogPost
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public int BlogID { get; set; }
|
||||||
|
|
||||||
|
public string BlogUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
35
CatherineLynwood/Models/BlogPostRequest.cs
Normal file
35
CatherineLynwood/Models/BlogPostRequest.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class BlogPostRequest
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public string AiSummary { get; set; }
|
||||||
|
|
||||||
|
public string BlogUrl { get; set; }
|
||||||
|
|
||||||
|
public string ContentBottom { get; set; }
|
||||||
|
|
||||||
|
public string ContentTop { get; set; }
|
||||||
|
|
||||||
|
public string ImageAlt { get; set; }
|
||||||
|
|
||||||
|
public string ImageDescription { get; set; }
|
||||||
|
|
||||||
|
public string ImagePosition { get; set; }
|
||||||
|
|
||||||
|
public string ImagePrompt { get; set; }
|
||||||
|
|
||||||
|
public string IndexText { get; set; }
|
||||||
|
|
||||||
|
public DateTime PublishDate { get; set; }
|
||||||
|
|
||||||
|
public int ResponderID { get; set; }
|
||||||
|
|
||||||
|
public string SubTitle { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
23
CatherineLynwood/Models/BlogSummaryResponse.cs
Normal file
23
CatherineLynwood/Models/BlogSummaryResponse.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class BlogSummaryResponse
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public string AiSummary { get; set; }
|
||||||
|
|
||||||
|
public string BlogUrl { get; set; }
|
||||||
|
|
||||||
|
public bool Draft { get; set; }
|
||||||
|
|
||||||
|
public string IndexText { get; set; }
|
||||||
|
|
||||||
|
public DateTime PublishDate { get; set; }
|
||||||
|
|
||||||
|
public string SubTitle { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
32
CatherineLynwood/Models/Reviews.cs
Normal file
32
CatherineLynwood/Models/Reviews.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace CatherineLynwood.Models
|
||||||
|
{
|
||||||
|
public class Review
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public string AuthorName { get; set; }
|
||||||
|
|
||||||
|
public DateTime DatePublished { get; set; }
|
||||||
|
|
||||||
|
public double RatingValue { get; set; }
|
||||||
|
|
||||||
|
public string ReviewBody { get; set; }
|
||||||
|
|
||||||
|
public string SiteName { get; set; }
|
||||||
|
|
||||||
|
public string URL { get; set; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Reviews
|
||||||
|
{
|
||||||
|
#region Public Properties
|
||||||
|
|
||||||
|
public List<Review> Items { get; set; } = new List<Review>();
|
||||||
|
|
||||||
|
public string SchemaJsonLd { get; set; }
|
||||||
|
|
||||||
|
#endregion Public Properties
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,6 +25,8 @@ namespace CatherineLynwood
|
|||||||
// Add IHttpContextAccessor for accessing HTTP context in tag helpers
|
// Add IHttpContextAccessor for accessing HTTP context in tag helpers
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
builder.Services.AddHostedService<IndexNowBackgroundService>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
// ✅ Add session services (in-memory only)
|
// ✅ Add session services (in-memory only)
|
||||||
@ -35,6 +37,20 @@ namespace CatherineLynwood
|
|||||||
options.Cookie.IsEssential = true;
|
options.Cookie.IsEssential = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ Add authentication with cookie settings
|
||||||
|
builder.Services.AddAuthentication("MyCookieAuth")
|
||||||
|
.AddCookie("MyCookieAuth", options =>
|
||||||
|
{
|
||||||
|
options.LoginPath = "/Account/Login";
|
||||||
|
options.LogoutPath = "/Account/Logout";
|
||||||
|
options.Cookie.Name = "MyAuthCookie";
|
||||||
|
options.Cookie.HttpOnly = true;
|
||||||
|
options.ExpireTimeSpan = TimeSpan.FromHours(12);
|
||||||
|
options.SlidingExpiration = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// Add RedirectsStore as singleton
|
// Add RedirectsStore as singleton
|
||||||
builder.Services.AddSingleton<RedirectsStore>(sp =>
|
builder.Services.AddSingleton<RedirectsStore>(sp =>
|
||||||
{
|
{
|
||||||
@ -44,13 +60,12 @@ namespace CatherineLynwood
|
|||||||
|
|
||||||
// ✅ Register the book access code service
|
// ✅ Register the book access code service
|
||||||
builder.Services.AddScoped<IAccessCodeService, AccessCodeService>();
|
builder.Services.AddScoped<IAccessCodeService, AccessCodeService>();
|
||||||
|
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<ChapterAudioMapCache>();
|
builder.Services.AddSingleton<ChapterAudioMapCache>();
|
||||||
builder.Services.AddHostedService<ChapterAudioMapService>();
|
builder.Services.AddHostedService<ChapterAudioMapService>();
|
||||||
builder.Services.AddSingleton<AudioTokenService>();
|
builder.Services.AddSingleton<AudioTokenService>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Add response compression services
|
// Add response compression services
|
||||||
builder.Services.AddResponseCompression(options =>
|
builder.Services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
@ -79,6 +94,10 @@ namespace CatherineLynwood
|
|||||||
.AddXmlMinification()
|
.AddXmlMinification()
|
||||||
.AddXhtmlMinification();
|
.AddXhtmlMinification();
|
||||||
|
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.Limits.MaxRequestBodySize = 40 * 1024 * 1024; // 40MB
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@ -89,22 +108,19 @@ namespace CatherineLynwood
|
|||||||
app.UseHsts(); // Adds the HSTS (HTTP Strict Transport Security) header
|
app.UseHsts(); // Adds the HSTS (HTTP Strict Transport Security) header
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseMiddleware<BlockPhpRequestsMiddleware>();
|
app.UseMiddleware<SpamAndSecurityMiddleware>();
|
||||||
app.UseMiddleware<BotFilterMiddleware>();
|
|
||||||
app.UseMiddleware<RedirectToWwwMiddleware>();
|
|
||||||
app.UseMiddleware<RefererValidationMiddleware>();
|
|
||||||
app.UseMiddleware<HoneypotLoggingMiddleware>();
|
app.UseMiddleware<HoneypotLoggingMiddleware>();
|
||||||
app.UseMiddleware<IpqsBlockMiddleware>();
|
|
||||||
|
|
||||||
app.UseMiddleware<RedirectMiddleware>();
|
app.UseMiddleware<RedirectMiddleware>();
|
||||||
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseWebMarkupMin();
|
app.UseWebMarkupMin();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseSession();
|
app.UseSession();
|
||||||
|
|
||||||
|
// ✅ Authentication must come before Authorization
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
using System.ComponentModel;
|
using CatherineLynwood.Models;
|
||||||
using System.Data;
|
|
||||||
|
|
||||||
using CatherineLynwood.Models;
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
namespace CatherineLynwood.Services
|
namespace CatherineLynwood.Services
|
||||||
{
|
{
|
||||||
public class DataAccess
|
public class DataAccess
|
||||||
@ -22,6 +27,181 @@ namespace CatherineLynwood.Services
|
|||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddBlogCommentAsync(BlogComment blogComment)
|
||||||
|
{
|
||||||
|
bool visible = false;
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "SaveBlogComment";
|
||||||
|
cmd.Parameters.AddWithValue("@BlogID", blogComment.BlogID);
|
||||||
|
cmd.Parameters.AddWithValue("@Comment", blogComment.Comment);
|
||||||
|
cmd.Parameters.AddWithValue("@EmailAddress", blogComment.EmailAddress);
|
||||||
|
cmd.Parameters.AddWithValue("@Name", blogComment.Name);
|
||||||
|
cmd.Parameters.AddWithValue("@Sex", blogComment.Sex);
|
||||||
|
cmd.Parameters.AddWithValue("@Age", blogComment.Age);
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
visible = GetDataBool(rdr, "Visible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Addhoneypot(DateTime dateTime, string ip, string country, string userAgent, string referer)
|
||||||
|
{
|
||||||
|
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 = "SaveHoneypot";
|
||||||
|
cmd.Parameters.AddWithValue("@DateTime", dateTime);
|
||||||
|
cmd.Parameters.AddWithValue("@IP", ip);
|
||||||
|
cmd.Parameters.AddWithValue("@Country", country);
|
||||||
|
cmd.Parameters.AddWithValue("@UserAgent", userAgent);
|
||||||
|
cmd.Parameters.AddWithValue("@Referer", referer);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddMarketingAsync(Marketing marketing)
|
||||||
|
{
|
||||||
|
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 = "SaveMarketingOptions";
|
||||||
|
cmd.Parameters.AddWithValue("@Name", marketing.Name);
|
||||||
|
cmd.Parameters.AddWithValue("@Email", marketing.Email);
|
||||||
|
cmd.Parameters.AddWithValue("@OptExtras", marketing.optExtras);
|
||||||
|
cmd.Parameters.AddWithValue("@OptFutureBooks", marketing.optFutureBooks);
|
||||||
|
cmd.Parameters.AddWithValue("@OptNews", marketing.optNews);
|
||||||
|
cmd.Parameters.AddWithValue("@Age", marketing.Age);
|
||||||
|
cmd.Parameters.AddWithValue("@Sex", marketing.Sex);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddQuestionAsync(Question question)
|
||||||
|
{
|
||||||
|
bool visible = false;
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "SaveQuestion";
|
||||||
|
cmd.Parameters.AddWithValue("@Question", question.Text);
|
||||||
|
cmd.Parameters.AddWithValue("@EmailAddress", question.EmailAddress);
|
||||||
|
cmd.Parameters.AddWithValue("@Name", question.Name);
|
||||||
|
cmd.Parameters.AddWithValue("@Sex", question.Sex);
|
||||||
|
cmd.Parameters.AddWithValue("@Age", question.Age);
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
visible = GetDataBool(rdr, "Visible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> EnterGiveawayAsync(Marketing marketing)
|
||||||
|
{
|
||||||
|
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 = "SaveGiveawayEntry";
|
||||||
|
cmd.Parameters.AddWithValue("@Name", marketing.Name);
|
||||||
|
cmd.Parameters.AddWithValue("@Email", marketing.Email);
|
||||||
|
cmd.Parameters.AddWithValue("@OptExtras", false);
|
||||||
|
cmd.Parameters.AddWithValue("@OptFutureBooks", true);
|
||||||
|
cmd.Parameters.AddWithValue("@OptNews", true);
|
||||||
|
cmd.Parameters.AddWithValue("@Age", marketing.Age);
|
||||||
|
cmd.Parameters.AddWithValue("@Sex", marketing.Sex);
|
||||||
|
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
public List<AccessCode> GetAccessCodes()
|
public List<AccessCode> GetAccessCodes()
|
||||||
{
|
{
|
||||||
List<AccessCode> accessCodes = new List<AccessCode>();
|
List<AccessCode> accessCodes = new List<AccessCode>();
|
||||||
@ -54,7 +234,6 @@ namespace CatherineLynwood.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,9 +241,9 @@ namespace CatherineLynwood.Services
|
|||||||
return accessCodes;
|
return accessCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Questions> GetQuestionsAsync()
|
public async Task<List<BlogSummaryResponse>> GetAllBlogsAsync()
|
||||||
{
|
{
|
||||||
Questions questions = new Questions();
|
List<BlogSummaryResponse> list = new List<BlogSummaryResponse>();
|
||||||
|
|
||||||
using (SqlConnection conn = new SqlConnection(_connectionString))
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
{
|
{
|
||||||
@ -75,37 +254,37 @@ 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 = "GetQuestions";
|
cmd.CommandText = "GetAllBlogs";
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
{
|
{
|
||||||
while (await rdr.ReadAsync())
|
while (await rdr.ReadAsync())
|
||||||
{
|
{
|
||||||
questions.AskedQuestions.Add(new Question
|
list.Add(new BlogSummaryResponse
|
||||||
{
|
{
|
||||||
Age = GetDataString(rdr, "Age"),
|
AiSummary = GetDataString(rdr, "AiSummary"),
|
||||||
EmailAddress = GetDataString(rdr, "EmailAddress"),
|
BlogUrl = GetDataString(rdr, "BlogUrl"),
|
||||||
Name = GetDataString(rdr, "Name"),
|
Draft = GetDataBool(rdr, "Draft"),
|
||||||
Text = GetDataString(rdr, "Question"),
|
IndexText = GetDataString(rdr, "IndexText"),
|
||||||
QuestionDate = GetDataDate(rdr, "QuestionDate"),
|
PublishDate = GetDataDate(rdr, "PublishDate"),
|
||||||
Sex = GetDataString(rdr, "Sex")
|
SubTitle = GetDataString(rdr, "SubTitle"),
|
||||||
|
Title = GetDataString(rdr, "Title"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return questions;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BlogIndex> GetBlogsAsync(string categoryIDs)
|
public async Task<ARCReaderList> GetAllARCReadersAsync()
|
||||||
{
|
{
|
||||||
BlogIndex blogIndex = new BlogIndex();
|
ARCReaderList arcReaderList = new ARCReaderList();
|
||||||
|
|
||||||
using (SqlConnection conn = new SqlConnection(_connectionString))
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
{
|
{
|
||||||
@ -116,116 +295,79 @@ 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 = "GetBlog";
|
cmd.CommandText = "GetAllARCReaders";
|
||||||
cmd.Parameters.AddWithValue("@CategoryIDs", categoryIDs);
|
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
{
|
{
|
||||||
while (await rdr.ReadAsync())
|
while (await rdr.ReadAsync())
|
||||||
{
|
{
|
||||||
blogIndex.BlogCategories.Add(new BlogCategory
|
arcReaderList.Applications.Add(new ARCReaderApplication
|
||||||
{
|
{
|
||||||
CategoryID = GetDataInt(rdr, "CategoryID"),
|
ApprovedSender = GetDataString(rdr, "ApprovedSender"),
|
||||||
Category = GetDataString(rdr, "Category"),
|
ContentFit = GetDataString(rdr, "ContentFit"),
|
||||||
Selected = GetDataBool(rdr, "Selected")
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await rdr.NextResultAsync();
|
return arcReaderList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BlogAdminIndex> GetBlogAdminIndexAsync()
|
||||||
|
{
|
||||||
|
BlogAdminIndex blogAdminIndex = new BlogAdminIndex();
|
||||||
|
blogAdminIndex.BlogItems = new List<BlogAdminIndexItem>();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetBlogIndex";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
while (await rdr.ReadAsync())
|
while (await rdr.ReadAsync())
|
||||||
{
|
{
|
||||||
blogIndex.Blogs.Add(new Blog
|
blogAdminIndex.BlogItems.Add(new BlogAdminIndexItem
|
||||||
{
|
{
|
||||||
BlogID = GetDataInt(rdr, "BlogID"),
|
BlogID = GetDataInt(rdr, "BlogID"),
|
||||||
BlogUrl = GetDataString(rdr, "BlogUrl"),
|
BlogUrl = GetDataString(rdr, "BlogUrl"),
|
||||||
Title = GetDataString(rdr, "Title"),
|
Draft = GetDataBool(rdr, "Draft"),
|
||||||
SubTitle = GetDataString(rdr, "SubTitle"),
|
|
||||||
PublishDate = GetDataDate(rdr, "PublishDate"),
|
|
||||||
Likes = GetDataInt(rdr, "Likes"),
|
|
||||||
IndexText = GetDataString(rdr, "IndexText"),
|
IndexText = GetDataString(rdr, "IndexText"),
|
||||||
ImageUrl = GetDataString(rdr, "ImageUrl"),
|
PublishDate = GetDataDate(rdr, "PublishDate"),
|
||||||
ImageAlt = GetDataString(rdr, "ImageAlt"),
|
SubTitle = GetDataString(rdr, "SubTitle"),
|
||||||
ImageDescription = GetDataString(rdr, "ImageDescription")
|
Title = GetDataString(rdr, "Title"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return blogIndex;
|
return blogAdminIndex;
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Blog> GetBlogItemAsync(string blogUrl)
|
|
||||||
{
|
|
||||||
Blog blog = new Blog();
|
|
||||||
|
|
||||||
using (SqlConnection conn = new SqlConnection(_connectionString))
|
|
||||||
{
|
|
||||||
using (SqlCommand cmd = new SqlCommand())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await conn.OpenAsync();
|
|
||||||
cmd.Connection = conn;
|
|
||||||
cmd.CommandType = CommandType.StoredProcedure;
|
|
||||||
cmd.CommandText = "GetBlogItem";
|
|
||||||
cmd.Parameters.AddWithValue("@BlogUrl", blogUrl);
|
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
|
||||||
{
|
|
||||||
while (await rdr.ReadAsync())
|
|
||||||
{
|
|
||||||
blog.AudioTeaserText = GetDataString(rdr, "AudioTeaserText");
|
|
||||||
blog.AudioTeaserUrl = GetDataString(rdr, "AudioTeaserUrl");
|
|
||||||
blog.AudioTranscriptUrl = GetDataString(rdr, "AudioTranscriptUrl");
|
|
||||||
blog.BlogID = GetDataInt(rdr, "BlogID");
|
|
||||||
blog.BlogUrl = GetDataString(rdr, "BlogUrl");
|
|
||||||
blog.ContentBottom = GetDataString(rdr, "ContentBottom");
|
|
||||||
blog.ContentTop = GetDataString(rdr, "ContentTop");
|
|
||||||
blog.ImageAlt = GetDataString(rdr, "ImageAlt");
|
|
||||||
blog.ImageDescription = GetDataString(rdr, "ImageDescription");
|
|
||||||
blog.ImageFirst = GetDataBool(rdr, "ImageFirst");
|
|
||||||
blog.ImageUrl = GetDataString(rdr, "ImageUrl");
|
|
||||||
blog.IndexText = GetDataString(rdr, "IndexText");
|
|
||||||
blog.Likes = GetDataInt(rdr, "Likes");
|
|
||||||
blog.PublishDate = GetDataDate(rdr, "PublishDate");
|
|
||||||
blog.SubTitle = GetDataString(rdr, "SubTitle");
|
|
||||||
blog.Template = GetDataString(rdr, "Template");
|
|
||||||
blog.Title = GetDataString(rdr, "Title");
|
|
||||||
blog.VideoUrl = GetDataString(rdr, "VideoUrl");
|
|
||||||
blog.PostedBy = GetDataString(rdr, "PostedBy");
|
|
||||||
blog.PostedImage = GetDataString(rdr, "PostedImage");
|
|
||||||
}
|
|
||||||
|
|
||||||
await rdr.NextResultAsync();
|
|
||||||
|
|
||||||
while (await rdr.ReadAsync())
|
|
||||||
{
|
|
||||||
blog.BlogImages.Add(new BlogImage
|
|
||||||
{
|
|
||||||
ImageID = GetDataInt(rdr, "ImageID"),
|
|
||||||
BlogID = GetDataInt(rdr, "BlogID"),
|
|
||||||
ImageUrl = GetDataString(rdr, "ImageUrl"),
|
|
||||||
ImageCaption = GetDataString(rdr, "ImageCaption"),
|
|
||||||
ImageText = GetDataString(rdr, "ImageText")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return blog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BlogComments> GetBlogCommentsAsync(int blogID)
|
public async Task<BlogComments> GetBlogCommentsAsync(int blogID)
|
||||||
@ -272,7 +414,6 @@ namespace CatherineLynwood.Services
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,9 +421,9 @@ namespace CatherineLynwood.Services
|
|||||||
return blogComments;
|
return blogComments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddBlogCommentAsync(BlogComment blogComment)
|
public async Task<Blog> GetBlogItemAsync(string blogUrl)
|
||||||
{
|
{
|
||||||
bool visible = false;
|
Blog blog = new Blog();
|
||||||
|
|
||||||
using (SqlConnection conn = new SqlConnection(_connectionString))
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
{
|
{
|
||||||
@ -293,33 +434,273 @@ 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 = "SaveBlogComment";
|
cmd.CommandText = "GetBlogItem";
|
||||||
cmd.Parameters.AddWithValue("@BlogID", blogComment.BlogID);
|
cmd.Parameters.AddWithValue("@BlogUrl", blogUrl);
|
||||||
cmd.Parameters.AddWithValue("@Comment", blogComment.Comment);
|
|
||||||
cmd.Parameters.AddWithValue("@EmailAddress", blogComment.EmailAddress);
|
|
||||||
cmd.Parameters.AddWithValue("@Name", blogComment.Name);
|
|
||||||
cmd.Parameters.AddWithValue("@Sex", blogComment.Sex);
|
|
||||||
cmd.Parameters.AddWithValue("@Age", blogComment.Age);
|
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
{
|
{
|
||||||
while (await rdr.ReadAsync())
|
while (await rdr.ReadAsync())
|
||||||
{
|
{
|
||||||
visible = GetDataBool(rdr, "Visible");
|
blog.AiSummary = GetDataString(rdr, "AiSummary");
|
||||||
|
blog.AudioTeaserText = GetDataString(rdr, "AudioTeaserText");
|
||||||
|
blog.AudioTeaserUrl = GetDataString(rdr, "AudioTeaserUrl");
|
||||||
|
blog.AudioTranscriptUrl = GetDataString(rdr, "AudioTranscriptUrl");
|
||||||
|
blog.BlogID = GetDataInt(rdr, "BlogID");
|
||||||
|
blog.BlogUrl = GetDataString(rdr, "BlogUrl");
|
||||||
|
blog.ContentBottom = GetDataString(rdr, "ContentBottom");
|
||||||
|
blog.ContentTop = GetDataString(rdr, "ContentTop");
|
||||||
|
blog.Draft = GetDataBool(rdr, "Draft");
|
||||||
|
blog.ImageAlt = GetDataString(rdr, "ImageAlt");
|
||||||
|
blog.ImageDescription = GetDataString(rdr, "ImageDescription");
|
||||||
|
blog.ImageFirst = GetDataBool(rdr, "ImageFirst");
|
||||||
|
blog.ImagePrompt = GetDataString(rdr, "ImagePrompt");
|
||||||
|
blog.ImageUrl = GetDataString(rdr, "ImageUrl");
|
||||||
|
blog.Indexed = GetDataBool(rdr, "Indexed");
|
||||||
|
blog.IndexText = GetDataString(rdr, "IndexText");
|
||||||
|
blog.Likes = GetDataInt(rdr, "Likes");
|
||||||
|
blog.PostedBy = GetDataString(rdr, "PostedBy");
|
||||||
|
blog.PostedImage = GetDataString(rdr, "PostedImage");
|
||||||
|
blog.PublishDate = GetDataDate(rdr, "PublishDate");
|
||||||
|
blog.SubTitle = GetDataString(rdr, "SubTitle");
|
||||||
|
blog.Template = GetDataString(rdr, "Template");
|
||||||
|
blog.Title = GetDataString(rdr, "Title");
|
||||||
|
blog.VideoUrl = GetDataString(rdr, "VideoUrl");
|
||||||
|
blog.ResponderID = GetDataInt(rdr, "ResponderID");
|
||||||
|
}
|
||||||
|
|
||||||
|
await rdr.NextResultAsync();
|
||||||
|
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
blog.BlogImages.Add(new BlogImage
|
||||||
|
{
|
||||||
|
ImageID = GetDataInt(rdr, "ImageID"),
|
||||||
|
BlogID = GetDataInt(rdr, "BlogID"),
|
||||||
|
ImageUrl = GetDataString(rdr, "ImageUrl"),
|
||||||
|
ImageCaption = GetDataString(rdr, "ImageCaption"),
|
||||||
|
ImageText = GetDataString(rdr, "ImageText")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return visible;
|
return blog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddMarketingAsync(Marketing marketing)
|
public async Task<BlogIndex> GetBlogsAsync()
|
||||||
|
{
|
||||||
|
BlogIndex blogIndex = new BlogIndex();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetBlog";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
blogIndex.Blogs.Add(new Blog
|
||||||
|
{
|
||||||
|
BlogID = GetDataInt(rdr, "BlogID"),
|
||||||
|
BlogUrl = GetDataString(rdr, "BlogUrl"),
|
||||||
|
Title = GetDataString(rdr, "Title"),
|
||||||
|
SubTitle = GetDataString(rdr, "SubTitle"),
|
||||||
|
PublishDate = GetDataDate(rdr, "PublishDate"),
|
||||||
|
Likes = GetDataInt(rdr, "Likes"),
|
||||||
|
IndexText = GetDataString(rdr, "IndexText"),
|
||||||
|
ImageUrl = GetDataString(rdr, "ImageUrl"),
|
||||||
|
ImageAlt = GetDataString(rdr, "ImageAlt"),
|
||||||
|
ImageDescription = GetDataString(rdr, "ImageDescription")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rdr.NextResultAsync();
|
||||||
|
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
blogIndex.NextBlog.PublishDate = GetDataDate(rdr, "PublishDate");
|
||||||
|
blogIndex.NextBlog.Title = GetDataString(rdr, "Title");
|
||||||
|
blogIndex.NextBlog.SubTitle = GetDataString(rdr, "SubTitle");
|
||||||
|
blogIndex.NextBlog.IndexText = GetDataString(rdr, "IndexText");
|
||||||
|
blogIndex.NextBlog.ImageAlt = GetDataString(rdr, "ImageAlt");
|
||||||
|
blogIndex.NextBlog.ImageUrl = GetDataString(rdr, "ImageUrl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blogIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<BlogPost>> GetDueBlogPostsAsync()
|
||||||
|
{
|
||||||
|
List<BlogPost> blogPosts = new List<BlogPost>();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
conn.Open();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetUnIndexedBlogs";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (rdr.Read())
|
||||||
|
{
|
||||||
|
blogPosts.Add(new BlogPost
|
||||||
|
{
|
||||||
|
BlogID = GetDataInt(rdr, "BlogID"),
|
||||||
|
BlogUrl = GetDataString(rdr, "BlogUrl")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blogPosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Questions> GetQuestionsAsync()
|
||||||
|
{
|
||||||
|
Questions questions = new Questions();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetQuestions";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
questions.AskedQuestions.Add(new Question
|
||||||
|
{
|
||||||
|
Age = GetDataString(rdr, "Age"),
|
||||||
|
EmailAddress = GetDataString(rdr, "EmailAddress"),
|
||||||
|
Name = GetDataString(rdr, "Name"),
|
||||||
|
Text = GetDataString(rdr, "Question"),
|
||||||
|
QuestionDate = GetDataDate(rdr, "QuestionDate"),
|
||||||
|
Sex = GetDataString(rdr, "Sex")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<SelectListItem>> GetResponderList()
|
||||||
|
{
|
||||||
|
List<SelectListItem> selectListItems = new List<SelectListItem>();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetResponderList";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
selectListItems.Add(new SelectListItem
|
||||||
|
{
|
||||||
|
Value = GetDataInt(rdr, "ResponderID").ToString(),
|
||||||
|
Text = GetDataString(rdr, "Name"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectListItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Reviews> GetReviewsAsync()
|
||||||
|
{
|
||||||
|
Reviews reviews = new Reviews();
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
{
|
||||||
|
using (SqlCommand cmd = new SqlCommand())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
cmd.Connection = conn;
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
cmd.CommandText = "GetReviews";
|
||||||
|
|
||||||
|
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await rdr.ReadAsync())
|
||||||
|
{
|
||||||
|
reviews.Items.Add(new Review
|
||||||
|
{
|
||||||
|
AuthorName = GetDataString(rdr, "AuthorName"),
|
||||||
|
DatePublished = GetDataDate(rdr, "DatePublished"),
|
||||||
|
RatingValue = GetDataDouble(rdr, "RatingValue"),
|
||||||
|
ReviewBody = GetDataString(rdr, "ReviewBody"),
|
||||||
|
SiteName = GetDataString(rdr, "SiteName"),
|
||||||
|
URL = GetDataString(rdr, "URL")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reviews;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> MarkAsNotifiedAsync(int blogID)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
|
||||||
@ -332,15 +713,8 @@ 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 = "SaveMarketingOptions";
|
cmd.CommandText = "MarkBlogAsIndexed";
|
||||||
cmd.Parameters.AddWithValue("@Name", marketing.Name);
|
cmd.Parameters.AddWithValue("@BlogID", blogID);
|
||||||
cmd.Parameters.AddWithValue("@Email", marketing.Email);
|
|
||||||
cmd.Parameters.AddWithValue("@OptExtras", marketing.optExtras);
|
|
||||||
cmd.Parameters.AddWithValue("@OptFutureBooks", marketing.optFutureBooks);
|
|
||||||
cmd.Parameters.AddWithValue("@OptNews", marketing.optNews);
|
|
||||||
cmd.Parameters.AddWithValue("@Age", marketing.Age);
|
|
||||||
cmd.Parameters.AddWithValue("@Sex", marketing.Sex);
|
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -353,7 +727,7 @@ namespace CatherineLynwood.Services
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Addhoneypot(DateTime dateTime, string ip, string country, string userAgent, string referer)
|
public async Task<bool> SaveBlogToDatabase(BlogPostRequest blog)
|
||||||
{
|
{
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
|
||||||
@ -366,18 +740,24 @@ 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 = "SaveHoneypot";
|
cmd.CommandText = "SaveBlog";
|
||||||
cmd.Parameters.AddWithValue("@DateTime", dateTime);
|
cmd.Parameters.AddWithValue("@AiSummary", blog.AiSummary);
|
||||||
cmd.Parameters.AddWithValue("@IP", ip);
|
cmd.Parameters.AddWithValue("@BlogUrl", blog.BlogUrl);
|
||||||
cmd.Parameters.AddWithValue("@Country", country);
|
cmd.Parameters.AddWithValue("@ContentBottom", blog.ContentBottom);
|
||||||
cmd.Parameters.AddWithValue("@UserAgent", userAgent);
|
cmd.Parameters.AddWithValue("@ContentTop", blog.ContentTop);
|
||||||
cmd.Parameters.AddWithValue("@Referer", referer);
|
cmd.Parameters.AddWithValue("@ImageAlt", blog.ImageAlt);
|
||||||
|
cmd.Parameters.AddWithValue("@ImageDescription", blog.ImageDescription);
|
||||||
|
cmd.Parameters.AddWithValue("@ImageFirst", blog.ImagePosition.ToLower() == "left");
|
||||||
|
cmd.Parameters.AddWithValue("@ImagePrompt", blog.ImagePrompt);
|
||||||
|
cmd.Parameters.AddWithValue("@IndexText", blog.IndexText);
|
||||||
|
cmd.Parameters.AddWithValue("@PublishDate", blog.PublishDate);
|
||||||
|
cmd.Parameters.AddWithValue("@ResponderID", blog.ResponderID);
|
||||||
|
cmd.Parameters.AddWithValue("@SubTitle", blog.SubTitle);
|
||||||
|
cmd.Parameters.AddWithValue("@Title", blog.Title);
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
success = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -385,9 +765,9 @@ namespace CatherineLynwood.Services
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AddQuestionAsync(Question question)
|
public async Task<bool> SaveContact(Contact contact)
|
||||||
{
|
{
|
||||||
bool visible = false;
|
bool success = true;
|
||||||
|
|
||||||
using (SqlConnection conn = new SqlConnection(_connectionString))
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
{
|
{
|
||||||
@ -398,29 +778,119 @@ 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 = "SaveQuestion";
|
cmd.CommandText = "SaveContact";
|
||||||
cmd.Parameters.AddWithValue("@Question", question.Text);
|
cmd.Parameters.AddWithValue("@Name", contact.Name);
|
||||||
cmd.Parameters.AddWithValue("@EmailAddress", question.EmailAddress);
|
cmd.Parameters.AddWithValue("@EmailAddress", contact.EmailAddress);
|
||||||
cmd.Parameters.AddWithValue("@Name", question.Name);
|
await cmd.ExecuteNonQueryAsync();
|
||||||
cmd.Parameters.AddWithValue("@Sex", question.Sex);
|
|
||||||
cmd.Parameters.AddWithValue("@Age", question.Age);
|
|
||||||
|
|
||||||
using (SqlDataReader rdr = await cmd.ExecuteReaderAsync())
|
|
||||||
{
|
|
||||||
while (await rdr.ReadAsync())
|
|
||||||
{
|
|
||||||
visible = GetDataBool(rdr, "Visible");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return visible;
|
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", arcReaderApplication.KindleEmail);
|
||||||
|
cmd.Parameters.AddWithValue("@ApprovedSender", arcReaderApplication.ApprovedSender ?? (object)DBNull.Value);
|
||||||
|
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> UpdateBlogAsync(Blog blog)
|
||||||
|
{
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
using (SqlConnection conn = new SqlConnection(_connectionString))
|
||||||
|
using (SqlCommand cmd = new SqlCommand("UpdateBlog", conn))
|
||||||
|
{
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
|
||||||
|
// Required ID for WHERE clause
|
||||||
|
cmd.Parameters.AddWithValue("@blogID", blog.BlogID);
|
||||||
|
|
||||||
|
// Basic text fields
|
||||||
|
cmd.Parameters.AddWithValue("@blogUrl", blog.BlogUrl ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@publishDate", blog.PublishDate);
|
||||||
|
cmd.Parameters.AddWithValue("@responderID", blog.ResponderID);
|
||||||
|
cmd.Parameters.AddWithValue("@title", blog.Title ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@subTitle", (object?)blog.SubTitle ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@indexText", blog.IndexText ?? (object)DBNull.Value);
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
cmd.Parameters.AddWithValue("@audioTranscriptUrl", (object?)blog.AudioTranscriptUrl ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@audioTeaserUrl", (object?)blog.AudioTeaserUrl ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@audioTeaserText", (object?)blog.AudioTeaserText ?? DBNull.Value);
|
||||||
|
|
||||||
|
// Numbers / bools
|
||||||
|
cmd.Parameters.AddWithValue("@likes", blog.Likes);
|
||||||
|
cmd.Parameters.AddWithValue("@imageFirst", blog.ImageFirst);
|
||||||
|
cmd.Parameters.AddWithValue("@indexed", blog.Indexed);
|
||||||
|
cmd.Parameters.AddWithValue("@draft", blog.Draft);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
cmd.Parameters.AddWithValue("@contentTop", blog.ContentTop ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@contentBottom", (object?)blog.ContentBottom ?? DBNull.Value);
|
||||||
|
|
||||||
|
// Media
|
||||||
|
cmd.Parameters.AddWithValue("@videoUrl", (object?)blog.VideoUrl ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@imageUrl", (object?)blog.ImageUrl ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@imageAlt", (object?)blog.ImageAlt ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@imageDescription", (object?)blog.ImageDescription ?? DBNull.Value);
|
||||||
|
|
||||||
|
// Template / AI
|
||||||
|
cmd.Parameters.AddWithValue("@template", blog.Template ?? (object)DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@aiSummary", (object?)blog.AiSummary ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("@imagePrompt", (object?)blog.ImagePrompt ?? DBNull.Value);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await conn.OpenAsync();
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
success = false;
|
||||||
|
// Optionally log ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Constructors
|
#endregion Public Constructors
|
||||||
@ -483,6 +953,13 @@ namespace CatherineLynwood.Services
|
|||||||
return rdr.IsDBNull(colIndex) ? 0 : rdr.GetDecimal(colIndex);
|
return rdr.IsDBNull(colIndex) ? 0 : rdr.GetDecimal(colIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Double GetDataDouble(SqlDataReader rdr, string field)
|
||||||
|
{
|
||||||
|
int colIndex = rdr.GetOrdinal(field);
|
||||||
|
|
||||||
|
return rdr.IsDBNull(colIndex) ? 0 : rdr.GetDouble(colIndex);
|
||||||
|
}
|
||||||
|
|
||||||
protected Int32 GetDataInt(SqlDataReader rdr, string field)
|
protected Int32 GetDataInt(SqlDataReader rdr, string field)
|
||||||
{
|
{
|
||||||
int colIndex = rdr.GetOrdinal(field);
|
int colIndex = rdr.GetOrdinal(field);
|
||||||
|
|||||||
113
CatherineLynwood/Services/IndexNowBackgroundService.cs
Normal file
113
CatherineLynwood/Services/IndexNowBackgroundService.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
namespace CatherineLynwood.Services
|
||||||
|
{
|
||||||
|
public class IndexNowBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<IndexNowBackgroundService> _logger;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly DataAccess _dataAccess;
|
||||||
|
|
||||||
|
public IndexNowBackgroundService(
|
||||||
|
ILogger<IndexNowBackgroundService> logger,
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IConfiguration configuration,
|
||||||
|
DataAccess dataAccess)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_configuration = configuration;
|
||||||
|
_dataAccess = dataAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("IndexNowBackgroundService is starting.");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Checking for due blog posts to notify at {Time}.", DateTime.UtcNow);
|
||||||
|
|
||||||
|
// 1️⃣ Fetch due blog posts
|
||||||
|
var duePosts = await _dataAccess.GetDueBlogPostsAsync();
|
||||||
|
|
||||||
|
if (duePosts != null && duePosts.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Found {Count} post(s) to notify.", duePosts.Count);
|
||||||
|
|
||||||
|
foreach (var post in duePosts)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Notifying IndexNow for post URL: {Url}", post.BlogUrl);
|
||||||
|
|
||||||
|
var success = await NotifyIndexNowAsync(post.BlogUrl);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Successfully notified IndexNow for URL: {Url}", post.BlogUrl);
|
||||||
|
|
||||||
|
await _dataAccess.MarkAsNotifiedAsync(post.BlogID);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to notify IndexNow for URL: {Url}", post.BlogUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("No posts due at this time.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error occurred while processing IndexNow notifications.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Wait for 1 minute before next poll
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("IndexNowBackgroundService is stopping.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> NotifyIndexNowAsync(string url)
|
||||||
|
{
|
||||||
|
var apiKey = _configuration["IndexNow:ApiKey"];
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
_logger.LogError("IndexNow API key is not configured.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint = $"https://api.indexnow.org/indexnow?url={Uri.EscapeDataString(url)}&key={apiKey}";
|
||||||
|
|
||||||
|
url = $"https://www.catherinelynwood.com/the-alpha-flame/blog/{url}";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var httpClient = _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
_logger.LogInformation("Sending IndexNow request for {Url}", url);
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync(endpoint);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("IndexNow notification successful for {Url}", url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("IndexNow notification failed for {Url} with status code {StatusCode}", url, response.StatusCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Exception occurred while notifying IndexNow for {Url}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
CatherineLynwood/Services/SmtpEmailService.cs
Normal file
68
CatherineLynwood/Services/SmtpEmailService.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using CatherineLynwood.Models;
|
||||||
|
|
||||||
|
using MailKit.Net.Smtp;
|
||||||
|
using MailKit.Security;
|
||||||
|
|
||||||
|
using MimeKit;
|
||||||
|
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
namespace CatherineLynwood.Services
|
||||||
|
{
|
||||||
|
public interface IEmailService
|
||||||
|
{
|
||||||
|
Task SendEmailAsync( string subject, string plainText, string htmlContent, Contact contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SmtpEmailService : IEmailService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public SmtpEmailService(IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendEmailAsync(string subject, string plainText, string htmlContent, Contact contact)
|
||||||
|
{
|
||||||
|
var message = new MimeMessage();
|
||||||
|
message.From.Add(new MailboxAddress("Catherine Lynwood", _config["Smtp:Sender"]));
|
||||||
|
message.To.Add(new MailboxAddress("Web Site", "catherine@catherinelynwood.com"));
|
||||||
|
message.ReplyTo.Add(new MailboxAddress(contact.Name, contact.EmailAddress));
|
||||||
|
|
||||||
|
message.Subject = subject;
|
||||||
|
|
||||||
|
message.Body = new BodyBuilder
|
||||||
|
{
|
||||||
|
HtmlBody = htmlContent,
|
||||||
|
TextBody = plainText
|
||||||
|
}.ToMessageBody();
|
||||||
|
|
||||||
|
using var client = new SmtpClient();
|
||||||
|
|
||||||
|
// Safer override: allow certs that are valid *except* for revocation check issues
|
||||||
|
client.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||||
|
{
|
||||||
|
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Allow only if the problem is revocation
|
||||||
|
if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors &&
|
||||||
|
chain.ChainStatus.All(status => status.Status == X509ChainStatusFlags.RevocationStatusUnknown ||
|
||||||
|
status.Status == X509ChainStatusFlags.OfflineRevocation))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, reject
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
await client.ConnectAsync(_config["Smtp:Host"], int.Parse(_config["Smtp:Port"]), SecureSocketOptions.StartTls);
|
||||||
|
await client.AuthenticateAsync(_config["Smtp:Username"], _config["Smtp:Password"]);
|
||||||
|
await client.SendAsync(message);
|
||||||
|
await client.DisconnectAsync(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,120 +8,122 @@ namespace CatherineLynwood.TagHelpers
|
|||||||
#region Public Properties
|
#region Public Properties
|
||||||
|
|
||||||
public DateTime? ArticleModifiedTime { get; set; }
|
public DateTime? ArticleModifiedTime { get; set; }
|
||||||
|
|
||||||
// Article specific properties
|
|
||||||
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 MetaKeywords { get; set; }
|
public string MetaKeywords { get; set; }
|
||||||
|
|
||||||
// Shared properties
|
|
||||||
public string MetaTitle { get; set; }
|
public string MetaTitle { get; set; }
|
||||||
|
|
||||||
public string MetaUrl { get; set; }
|
public string MetaUrl { get; set; }
|
||||||
|
|
||||||
// Open Graph specific properties
|
|
||||||
public string OgSiteName { get; set; }
|
public string OgSiteName { get; set; }
|
||||||
|
|
||||||
public string OgType { get; set; } = "article";
|
public string OgType { get; set; } = "article";
|
||||||
|
|
||||||
// Twitter specific properties
|
|
||||||
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 Public Properties
|
#endregion
|
||||||
|
|
||||||
// Optional: a full URL to a video (e.g., MP4 or embedded player)
|
|
||||||
|
|
||||||
#region Public Methods
|
#region Public Methods
|
||||||
|
|
||||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||||
{
|
{
|
||||||
output.TagName = null; // Suppress output tag
|
output.TagName = null; // Remove wrapper tag
|
||||||
var metaTags = new System.Text.StringBuilder();
|
var metaTags = new System.Text.StringBuilder();
|
||||||
|
|
||||||
// General meta tags
|
// General meta tags
|
||||||
if (!string.IsNullOrWhiteSpace(MetaDescription))
|
if (!string.IsNullOrWhiteSpace(MetaDescription))
|
||||||
metaTags.AppendLine($"<meta name=\"description\" content=\"{MetaDescription}\">");
|
metaTags.AppendLine($"<meta name=\"description\" content=\"{MetaDescription}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaKeywords))
|
if (!string.IsNullOrWhiteSpace(MetaKeywords))
|
||||||
metaTags.AppendLine($"<meta name=\"keywords\" content=\"{MetaKeywords}\">");
|
metaTags.AppendLine($"<meta name=\"keywords\" content=\"{MetaKeywords}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaAuthor))
|
if (!string.IsNullOrWhiteSpace(MetaAuthor))
|
||||||
metaTags.AppendLine($"<meta name=\"author\" content=\"{MetaAuthor}\">");
|
metaTags.AppendLine($"<meta name=\"author\" content=\"{MetaAuthor}\">");
|
||||||
|
|
||||||
// Open Graph meta tags
|
// Open Graph meta tags
|
||||||
if (!string.IsNullOrWhiteSpace(MetaTitle))
|
if (!string.IsNullOrWhiteSpace(MetaTitle))
|
||||||
metaTags.AppendLine($"<meta property=\"og:title\" content=\"{MetaTitle}\">");
|
metaTags.AppendLine($"<meta property=\"og:title\" content=\"{MetaTitle}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaDescription))
|
if (!string.IsNullOrWhiteSpace(MetaDescription))
|
||||||
metaTags.AppendLine($"<meta property=\"og:description\" content=\"{MetaDescription}\">");
|
metaTags.AppendLine($"<meta property=\"og:description\" content=\"{MetaDescription}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(OgType))
|
if (!string.IsNullOrWhiteSpace(OgType))
|
||||||
metaTags.AppendLine($"<meta property=\"og:type\" content=\"{OgType}\">");
|
metaTags.AppendLine($"<meta property=\"og:type\" content=\"{OgType}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaUrl))
|
if (!string.IsNullOrWhiteSpace(MetaUrl))
|
||||||
metaTags.AppendLine($"<meta property=\"og:url\" content=\"{MetaUrl}\">");
|
metaTags.AppendLine($"<meta property=\"og:url\" content=\"{MetaUrl}\">");
|
||||||
|
|
||||||
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(MetaImageAlt))
|
if (!string.IsNullOrWhiteSpace(MetaImageAlt))
|
||||||
metaTags.AppendLine($"<meta property=\"og:image:alt\" content=\"{MetaImageAlt}\">");
|
metaTags.AppendLine($"<meta property=\"og:image:alt\" content=\"{MetaImageAlt}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(OgSiteName))
|
if (!string.IsNullOrWhiteSpace(OgSiteName))
|
||||||
metaTags.AppendLine($"<meta property=\"og:site_name\" content=\"{OgSiteName}\">");
|
metaTags.AppendLine($"<meta property=\"og:site_name\" content=\"{OgSiteName}\">");
|
||||||
|
|
||||||
if (ArticlePublishedTime.HasValue)
|
if (ArticlePublishedTime.HasValue)
|
||||||
metaTags.AppendLine($"<meta property=\"article:published_time\" content=\"{ArticlePublishedTime.Value:yyyy-MM-ddTHH:mm:ssZ}\">");
|
metaTags.AppendLine($"<meta property=\"article:published_time\" content=\"{ArticlePublishedTime.Value:yyyy-MM-ddTHH:mm:ssZ}\">");
|
||||||
|
|
||||||
if (ArticleModifiedTime.HasValue)
|
if (ArticleModifiedTime.HasValue)
|
||||||
metaTags.AppendLine($"<meta property=\"article:modified_time\" content=\"{ArticleModifiedTime.Value:yyyy-MM-ddTHH:mm:ssZ}\">");
|
metaTags.AppendLine($"<meta property=\"article:modified_time\" content=\"{ArticleModifiedTime.Value:yyyy-MM-ddTHH:mm:ssZ}\">");
|
||||||
|
|
||||||
// Twitter meta tags
|
// Twitter meta tags
|
||||||
if (!string.IsNullOrWhiteSpace(TwitterCardType))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:card\" content=\"{TwitterCardType}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaTitle))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:title\" content=\"{MetaTitle}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaDescription))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:description\" content=\"{MetaDescription}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(TwitterSiteHandle))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:site\" content=\"{TwitterSiteHandle}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(TwitterCreatorHandle))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:creator\" content=\"{TwitterCreatorHandle}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaImage))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:image\" content=\"{MetaImage}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaImageAlt))
|
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:image:alt\" content=\"{MetaImageAlt}\">");
|
|
||||||
if (!string.IsNullOrWhiteSpace(TwitterVideoUrl))
|
if (!string.IsNullOrWhiteSpace(TwitterVideoUrl))
|
||||||
{
|
{
|
||||||
|
// Ensure absolute URL for player
|
||||||
|
var playerUrl = TwitterVideoUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TwitterVideoUrl
|
||||||
|
: $"https://www.catherinelynwood.com/{TwitterVideoUrl.TrimStart('/')}";
|
||||||
|
|
||||||
metaTags.AppendLine("<meta name=\"twitter:card\" content=\"player\">");
|
metaTags.AppendLine("<meta name=\"twitter:card\" content=\"player\">");
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:player\" content=\"{TwitterVideoUrl}\">");
|
metaTags.AppendLine($"<meta name=\"twitter:player\" content=\"{playerUrl}\">");
|
||||||
|
|
||||||
if (TwitterPlayerWidth.HasValue)
|
if (TwitterPlayerWidth.HasValue)
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:player:width\" content=\"{TwitterPlayerWidth}\">");
|
metaTags.AppendLine($"<meta name=\"twitter:player:width\" content=\"{TwitterPlayerWidth}\">");
|
||||||
|
|
||||||
if (TwitterPlayerHeight.HasValue)
|
if (TwitterPlayerHeight.HasValue)
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:player:height\" content=\"{TwitterPlayerHeight}\">");
|
metaTags.AppendLine($"<meta name=\"twitter:player:height\" content=\"{TwitterPlayerHeight}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaImage))
|
if (!string.IsNullOrWhiteSpace(MetaImage))
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:image\" content=\"{MetaImage}\">");
|
metaTags.AppendLine($"<meta name=\"twitter:image\" content=\"{MetaImage}\">");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(MetaImageAlt))
|
||||||
|
metaTags.AppendLine($"<meta name=\"twitter:image:alt\" content=\"{MetaImageAlt}\">");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:card\" content=\"{TwitterCardType}\">");
|
metaTags.AppendLine($"<meta name=\"twitter:card\" content=\"{TwitterCardType}\">");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(MetaTitle))
|
||||||
|
metaTags.AppendLine($"<meta name=\"twitter:title\" content=\"{MetaTitle}\">");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(MetaDescription))
|
||||||
|
metaTags.AppendLine($"<meta name=\"twitter:description\" content=\"{MetaDescription}\">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(MetaImage))
|
if (!string.IsNullOrWhiteSpace(MetaImage))
|
||||||
metaTags.AppendLine($"<meta name=\"twitter:image\" content=\"{MetaImage}\">");
|
metaTags.AppendLine($"<meta name=\"twitter:image\" content=\"{MetaImage}\">");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(MetaImageAlt))
|
||||||
|
metaTags.AppendLine($"<meta name=\"twitter:image:alt\" content=\"{MetaImageAlt}\">");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output all content
|
if (!string.IsNullOrWhiteSpace(TwitterSiteHandle))
|
||||||
|
metaTags.AppendLine($"<meta name=\"twitter:site\" content=\"{TwitterSiteHandle}\">");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(TwitterCreatorHandle))
|
||||||
|
metaTags.AppendLine($"<meta name=\"twitter:creator\" content=\"{TwitterCreatorHandle}\">");
|
||||||
|
|
||||||
output.Content.SetHtmlContent(metaTags.ToString());
|
output.Content.SetHtmlContent(metaTags.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Public Methods
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
71
CatherineLynwood/Views/Account/Login.cshtml
Normal file
71
CatherineLynwood/Views/Account/Login.cshtml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Login";
|
||||||
|
var returnUrl = ViewBag.ReturnUrl ?? "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-header bg-dark text-light text-center">
|
||||||
|
<h4 class="mb-0">Admin Login</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if (ViewBag.Error != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@ViewBag.Error</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form method="post" class="needs-validation" novalidate>
|
||||||
|
<input type="hidden" name="returnUrl" value="@returnUrl" />
|
||||||
|
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Please enter your username.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-4">
|
||||||
|
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
Please enter your password.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
// Enable Bootstrap client-side validation
|
||||||
|
(() => {
|
||||||
|
'use strict'
|
||||||
|
const forms = document.querySelectorAll('.needs-validation')
|
||||||
|
Array.from(forms).forEach(form => {
|
||||||
|
form.addEventListener('submit', event => {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Meta{
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
}
|
||||||
63
CatherineLynwood/Views/Admin/ArcReaders.cshtml
Normal file
63
CatherineLynwood/Views/Admin/ArcReaders.cshtml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
@model CatherineLynwood.Models.ARCReaderList
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "ARC Reader List";
|
||||||
|
}
|
||||||
|
|
||||||
|
<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="Admin" asp-action="Index">Admin Centre</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">ARC Readers</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h1>ARC Readers List</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||||
|
@foreach (var item in Model.Applications)
|
||||||
|
{
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100 shadow-sm">
|
||||||
|
<div class="card-header bg-info text-black">
|
||||||
|
<h5 class="card-title">@item.FullName</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<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>Approved Sender:</strong> @item.ApprovedSender</p>
|
||||||
|
<p class="card-text mb-1"><strong>Platforms:</strong> @item.Platforms</p>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.PlatformsOther))
|
||||||
|
{
|
||||||
|
<p class="card-text mb-1"><strong>Platforms Other:</strong> @item.PlatformsOther</p>
|
||||||
|
}
|
||||||
|
<p class="card-text mb-1"><strong>Preview Chapters:</strong> @item.PreviewChapters</p>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.ReviewLink))
|
||||||
|
{
|
||||||
|
<p class="card-text mb-1"><strong>Review Link:</strong> <a href="@item.ReviewLink" target="_blank">@item.ReviewLink</a></p>
|
||||||
|
}
|
||||||
|
<p class="card-text mb-1"><strong>Content Fit:</strong> @item.ContentFit</p>
|
||||||
|
<p class="card-text mb-1"><strong>Review Commitment:</strong> @item.ReviewCommitment</p>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(item.ExtraNotes))
|
||||||
|
{
|
||||||
|
<p class="card-text mb-1"><strong>Extra Notes:</strong> @item.ExtraNotes</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-muted">
|
||||||
|
Submitted on @item.SubmittedAt.ToString("d MMM yyyy")
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
72
CatherineLynwood/Views/Admin/Blog.cshtml
Normal file
72
CatherineLynwood/Views/Admin/Blog.cshtml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
@model CatherineLynwood.Models.BlogAdminIndex
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Blog Admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
<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="Admin" asp-action="Index">Admin Centre</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Blog Index</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||||
|
@foreach (var item in Model.BlogItems)
|
||||||
|
{
|
||||||
|
string rowCSS = "bg-success text-white";
|
||||||
|
string textCSS = "text-white";
|
||||||
|
if (item.Draft)
|
||||||
|
{
|
||||||
|
rowCSS = "bg-warning text-black";
|
||||||
|
textCSS = "text-black";
|
||||||
|
}
|
||||||
|
else if (item.PublishDate > DateTime.Now)
|
||||||
|
{
|
||||||
|
rowCSS = "bg-info text-black";
|
||||||
|
textCSS = "text-black";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100 shadow-sm">
|
||||||
|
<div class="card-header @rowCSS">
|
||||||
|
<div class="row align-items-center p-2 rounded-3">
|
||||||
|
<div class="col-8">
|
||||||
|
<a href="/the-alpha-flame/blog/@item.BlogUrl" class="@textCSS text-decoration-none">@item.Title</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 @textCSS">
|
||||||
|
@item.PublishDate.ToShortDateString()
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col">
|
||||||
|
<div>
|
||||||
|
<em>@item.SubTitle</em>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 my-2"><hr /></div>
|
||||||
|
<div>
|
||||||
|
@item.IndexText
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<a asp-controller="Admin" asp-action="BlogEdit" asp-route-slug="@item.BlogUrl" class="btn btn-dark">Edit</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
214
CatherineLynwood/Views/Admin/BlogEdit.cshtml
Normal file
214
CatherineLynwood/Views/Admin/BlogEdit.cshtml
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
@model CatherineLynwood.Models.Blog
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = $"Edit: {Model.Title}";
|
||||||
|
}
|
||||||
|
|
||||||
|
<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="Admin" asp-action="Index">Admin Centre</a></li>
|
||||||
|
<li class="breadcrumb-item"><a asp-controller="Admin" asp-action="Blog">Blog Index</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">@Model.Title</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<form asp-action="BlogEdit" method="post" class="col-12" enctype="multipart/form-data">
|
||||||
|
@* Validation summary *@
|
||||||
|
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-8 col-md-10">
|
||||||
|
<h1>Edit Blog Post</h1>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid col-4 col-md-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<h5 class="mt-4">Basic Info</h5>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label asp-for="BlogID" class="form-label"></label>
|
||||||
|
<input asp-for="BlogID" class="form-control" readonly />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7">
|
||||||
|
<label asp-for="BlogUrl" class="form-label"></label>
|
||||||
|
<input asp-for="BlogUrl" class="form-control" />
|
||||||
|
<span asp-validation-for="BlogUrl" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label asp-for="PublishDate" class="form-label"></label>
|
||||||
|
<input asp-for="PublishDate" type="datetime-local" class="form-control" />
|
||||||
|
<span asp-validation-for="PublishDate" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row align-items-center mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label asp-for="ResponderID" class="form-label"></label>
|
||||||
|
<select asp-for="ResponderID" asp-items="ViewBag.ResponderList" class="form-control"></select>
|
||||||
|
<span asp-validation-for="ResponderID" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<label asp-for="Likes" class="form-label"></label>
|
||||||
|
<input asp-for="Likes" class="form-control" />
|
||||||
|
<span asp-validation-for="Likes" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label asp-for="Template" class="form-label"></label>
|
||||||
|
<select asp-for="Template" class="form-control">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="slideshow">Slideshow</option>
|
||||||
|
</select>
|
||||||
|
<span asp-validation-for="Template" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 pt-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input asp-for="Indexed" class="form-check-input" type="checkbox" />
|
||||||
|
<label asp-for="Indexed" class="form-check-label"></label>
|
||||||
|
<span asp-validation-for="Indexed" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 pt-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input asp-for="Draft" class="form-check-input" type="checkbox" />
|
||||||
|
<label asp-for="Draft" class="form-check-label"></label>
|
||||||
|
<span asp-validation-for="Draft" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label asp-for="Title" class="form-label"></label>
|
||||||
|
<input asp-for="Title" class="form-control" />
|
||||||
|
<span asp-validation-for="Title" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label asp-for="SubTitle" class="form-label"></label>
|
||||||
|
<input asp-for="SubTitle" class="form-control" />
|
||||||
|
<span asp-validation-for="SubTitle" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<h5 class="mt-4">Content</h5>
|
||||||
|
<div class="mb-3 form-floating">
|
||||||
|
<textarea asp-for="IndexText" class="form-control" style="height: 100px"></textarea>
|
||||||
|
<label asp-for="IndexText"></label>
|
||||||
|
<span asp-validation-for="IndexText" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-floating">
|
||||||
|
<textarea asp-for="ContentTop" class="form-control" style="height: 150px"></textarea>
|
||||||
|
<label asp-for="ContentTop"></label>
|
||||||
|
<span asp-validation-for="ContentTop" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-floating">
|
||||||
|
<textarea asp-for="ContentBottom" class="form-control" style="height: 150px"></textarea>
|
||||||
|
<label asp-for="ContentBottom"></label>
|
||||||
|
<span asp-validation-for="ContentBottom" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image -->
|
||||||
|
<h5 class="mt-4">Image</h5>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="ImageUrl" class="form-label"></label>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<span class="input-group-text"><i class="fad fa-camera"></i></span>
|
||||||
|
<input asp-for="ImageUrl" class="form-control" readonly />
|
||||||
|
</div>
|
||||||
|
<input name="ImageUpload" type="file" accept=".png" class="form-control" />
|
||||||
|
<span asp-validation-for="ImageUrl" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="ImageAlt" class="form-label"></label>
|
||||||
|
<textarea asp-for="ImageAlt" class="form-control" style="height: 85px;"></textarea>
|
||||||
|
<span asp-validation-for="ImageAlt" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="ImageDescription" class="form-label"></label>
|
||||||
|
<textarea asp-for="ImageDescription" class="form-control" style="height: 85px;"></textarea>
|
||||||
|
<span asp-validation-for="ImageDescription" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input asp-for="ImageFirst" class="form-check-input" type="checkbox" />
|
||||||
|
<label asp-for="ImageFirst" class="form-check-label"></label>
|
||||||
|
<span asp-validation-for="ImageFirst" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio / Video -->
|
||||||
|
<h5 class="mt-4">Audio / Video</h5>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="AudioTranscriptUrl" class="form-label"></label>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<span class="input-group-text"><i class="fad fa-microphone"></i></span>
|
||||||
|
<input asp-for="AudioTranscriptUrl" class="form-control" readonly />
|
||||||
|
</div>
|
||||||
|
<input name="AudioTranscriptUpload" type="file" accept=".mp3" class="form-control" />
|
||||||
|
<span asp-validation-for="AudioTranscriptUrl" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="AudioTeaserUrl" class="form-label"></label>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<span class="input-group-text"><i class="fad fa-microphone"></i></span>
|
||||||
|
<input asp-for="AudioTeaserUrl" class="form-control" readonly />
|
||||||
|
</div>
|
||||||
|
<input name="AudioTeaserUpload" type="file" accept=".mp3" class="form-control" />
|
||||||
|
<span asp-validation-for="AudioTeaserUrl" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="VideoUrl" class="form-label"></label>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<span class="input-group-text"><i class="fad fa-camera-movie"></i></span>
|
||||||
|
<input asp-for="VideoUrl" class="form-control" readonly />
|
||||||
|
</div>
|
||||||
|
<input name="VideoUpload" type="file" accept=".mp4" class="form-control" />
|
||||||
|
<span asp-validation-for="VideoUrl" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-floating">
|
||||||
|
<textarea asp-for="AudioTeaserText" class="form-control" style="height: 100px"></textarea>
|
||||||
|
<label asp-for="AudioTeaserText"></label>
|
||||||
|
<span asp-validation-for="AudioTeaserText" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AI / Prompt -->
|
||||||
|
<h5 class="mt-4">AI / Prompt</h5>
|
||||||
|
<div class="mb-3 form-floating">
|
||||||
|
<textarea asp-for="AiSummary" class="form-control" style="height: 100px"></textarea>
|
||||||
|
<label asp-for="AiSummary"></label>
|
||||||
|
<span asp-validation-for="AiSummary" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-floating">
|
||||||
|
<textarea asp-for="ImagePrompt" class="form-control" style="height: 100px"></textarea>
|
||||||
|
<label asp-for="ImagePrompt"></label>
|
||||||
|
<span asp-validation-for="ImagePrompt" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="d-grid col-4 col-md-2 offset-8 offset-md-10">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
21
CatherineLynwood/Views/Admin/Index.cshtml
Normal file
21
CatherineLynwood/Views/Admin/Index.cshtml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Site Admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<h1>Admin Centre</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="list-group">
|
||||||
|
<a asp-action="ArcReaders" class="list-group-item list-group-item-action">ARC Reader Admin</a>
|
||||||
|
<a asp-action="Blog" class="list-group-item list-group-item-action">Blog Admin</a>
|
||||||
|
|
||||||
|
@* <a href="#" class="list-group-item list-group-item-action">A third link item</a>
|
||||||
|
<a href="#" class="list-group-item list-group-item-action">A fourth link item</a>
|
||||||
|
<a class="list-group-item list-group-item-action disabled">A disabled link item</a> *@
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -32,13 +32,8 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1>Ask A Question</h1>
|
<h1>Ask A Question</h1>
|
||||||
</div>
|
</div>
|
||||||
@if (ViewData["VpnWarning"] is true)
|
|
||||||
{
|
|
||||||
<partial name="_VPNWarning" />
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
The questions shown below have already been suggested, and are ones I plan to respond to in my upcoming podcast. If you have a question you'd like me
|
The questions shown below have already been asked, and are ones I plan to respond to in my upcoming blog posts. If you have a question you'd like me
|
||||||
to answer, please use the form below to ask it. Try to make sure no one else has asked it previously.
|
to answer, please use the form below to ask it. Try to make sure no one else has asked it previously.
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@ -69,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form asp-action="AskQuestion" class="row" id="question">
|
<form asp-action="AskQuestion" class="row" id="question" onsubmit="disableSubmit(this)">
|
||||||
<div class="col-12 pb-3">
|
<div class="col-12 pb-3">
|
||||||
<h2>Ask A Question</h2>
|
<h2>Ask A Question</h2>
|
||||||
In the future I intend to record a question and answer podcast. The idea is that you will be able to ask me anything about the
|
In the future I intend to record a question and answer podcast. The idea is that you will be able to ask me anything about the
|
||||||
@ -152,50 +147,14 @@
|
|||||||
|
|
||||||
<!-- Placeholder Button and Submit Button -->
|
<!-- Placeholder Button and Submit Button -->
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
@if (ViewData["VpnWarning"] is true)
|
<button type="button" class="btn btn-secondary mb-3" id="fakeSubmitButton">Ask Question</button>
|
||||||
{
|
<button type="submit" class="btn btn-dark mb-3 d-none" id="submitButton">Ask Question</button>
|
||||||
<button type="button" class="btn btn-secondary mb-3" disabled>Ask Question</button>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<button type="button" class="btn btn-secondary mb-3" id="fakeSubmitButton">Ask Question</button>
|
|
||||||
<button type="submit" class="btn btn-dark mb-3 d-none" id="submitButton">Ask Question</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
// JavaScript to handle checkbox and button toggle functionality
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const agreeCheckbox = document.getElementById("agreeTerms");
|
|
||||||
const fakeSubmitButton = document.getElementById("fakeSubmitButton");
|
|
||||||
const submitButton = document.getElementById("submitButton");
|
|
||||||
const termsWarning = document.getElementById("termsWarning");
|
|
||||||
|
|
||||||
// Toggle buttons based on checkbox state
|
|
||||||
agreeCheckbox.addEventListener("change", function () {
|
|
||||||
if (this.checked) {
|
|
||||||
fakeSubmitButton.classList.add("d-none"); // Hide placeholder button
|
|
||||||
submitButton.classList.remove("d-none"); // Show real submit button
|
|
||||||
termsWarning.classList.add("d-none"); // Hide warning
|
|
||||||
} else {
|
|
||||||
fakeSubmitButton.classList.remove("d-none"); // Show placeholder button
|
|
||||||
submitButton.classList.add("d-none"); // Hide real submit button
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show warning if placeholder button is clicked
|
|
||||||
fakeSubmitButton.addEventListener("click", function () {
|
|
||||||
termsWarning.classList.remove("d-none"); // Show warning
|
|
||||||
agreeCheckbox.focus(); // Focus on checkbox
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@section Meta {
|
@section Meta {
|
||||||
<meta name="description" content="Have a question for Catherine Lynwood? Ask anything about her books, characters, writing process, or themes explored in The Alpha Flame. Get involved today!">
|
<meta name="description" content="Have a question for Catherine Lynwood? Ask anything about her books, characters, writing process, or themes explored in The Alpha Flame. Get involved today!">
|
||||||
|
|
||||||
@ -204,3 +163,44 @@
|
|||||||
<meta name="author" content="Catherine Lynwood">
|
<meta name="author" content="Catherine Lynwood">
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Scripts{
|
||||||
|
<script>
|
||||||
|
// JavaScript to handle checkbox and button toggle functionality
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const agreeCheckbox = document.getElementById("agreeTerms");
|
||||||
|
const fakeSubmitButton = document.getElementById("fakeSubmitButton");
|
||||||
|
const submitButton = document.getElementById("submitButton");
|
||||||
|
const termsWarning = document.getElementById("termsWarning");
|
||||||
|
|
||||||
|
// Toggle buttons based on checkbox state
|
||||||
|
agreeCheckbox.addEventListener("change", function () {
|
||||||
|
if (this.checked) {
|
||||||
|
fakeSubmitButton.classList.add("d-none"); // Hide placeholder button
|
||||||
|
submitButton.classList.remove("d-none"); // Show real submit button
|
||||||
|
termsWarning.classList.add("d-none"); // Hide warning
|
||||||
|
} else {
|
||||||
|
fakeSubmitButton.classList.remove("d-none"); // Show placeholder button
|
||||||
|
submitButton.classList.add("d-none"); // Hide real submit button
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show warning if placeholder button is clicked
|
||||||
|
fakeSubmitButton.addEventListener("click", function () {
|
||||||
|
termsWarning.classList.remove("d-none"); // Show warning
|
||||||
|
agreeCheckbox.focus(); // Focus on checkbox
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function disableSubmit(form) {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = 'Sending...'; // Optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
}
|
||||||
@ -88,8 +88,8 @@
|
|||||||
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-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-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-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/chapters/chapter-1-beth"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-1-beth"
|
||||||
meta-image="https://www.catherinelynwood.com/images/webp/beth-12-600-600.webp"
|
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"
|
meta-image-alt="Beth from 'The Alpha Flame' 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)"
|
||||||
@ -105,7 +105,7 @@
|
|||||||
"@@context": "https://schema.org",
|
"@@context": "https://schema.org",
|
||||||
"@@type": "Chapter",
|
"@@type": "Chapter",
|
||||||
"name": "Chapter 1: Drowning in Silence – Beth",
|
"name": "Chapter 1: Drowning in Silence – Beth",
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/chapters/chapter-1-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.",
|
"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,
|
"position": 1,
|
||||||
"inLanguage": "en-GB",
|
"inLanguage": "en-GB",
|
||||||
|
|||||||
@ -104,8 +104,8 @@
|
|||||||
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-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-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-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/chapters/chapter-2-maggie"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-13-susie"
|
||||||
meta-image="https://www.catherinelynwood.com/images/webp/maggie-grant-43-600.webp"
|
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"
|
meta-image-alt="Maggie from 'The Alpha Flame' 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)"
|
||||||
@ -121,7 +121,7 @@
|
|||||||
"@@context": "https://schema.org",
|
"@@context": "https://schema.org",
|
||||||
"@@type": "Chapter",
|
"@@type": "Chapter",
|
||||||
"name": "Chapter 13: A Name She Never Owned - Susie",
|
"name": "Chapter 13: A Name She Never Owned - Susie",
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/chapters/chapter-2-maggie",
|
"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.",
|
"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,
|
"position": 13,
|
||||||
"inLanguage": "en-GB",
|
"inLanguage": "en-GB",
|
||||||
|
|||||||
@ -87,8 +87,8 @@
|
|||||||
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-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-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-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/chapters/chapter-2-maggie"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery/chapters/chapter-2-maggie"
|
||||||
meta-image="https://www.catherinelynwood.com/images/webp/maggie-grant-43-600.webp"
|
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"
|
meta-image-alt="Maggie from 'The Alpha Flame' 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)"
|
||||||
@ -104,7 +104,7 @@
|
|||||||
"@@context": "https://schema.org",
|
"@@context": "https://schema.org",
|
||||||
"@@type": "Chapter",
|
"@@type": "Chapter",
|
||||||
"name": "Chapter 2: The Last Lesson – Maggie",
|
"name": "Chapter 2: The Last Lesson – Maggie",
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/chapters/chapter-2-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.",
|
"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,
|
"position": 2,
|
||||||
"inLanguage": "en-GB",
|
"inLanguage": "en-GB",
|
||||||
|
|||||||
@ -53,9 +53,7 @@
|
|||||||
<a asp-controller="Discovery" asp-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a>
|
<a asp-controller="Discovery" asp-action="Listen" class="btn btn-dark btn-sm">Listen to the Book</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
@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">Scrapbook: Maggie’s Designs</h5>
|
<h5 class="card-title">Scrapbook: Maggie’s Designs</h5>
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
@{
|
@model CatherineLynwood.Models.Reviews
|
||||||
|
|
||||||
|
@{
|
||||||
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
|
ViewData["Title"] = "The Alpha Flame: A Gritty 1980s Birmingham Crime Novel about Twin Sisters";
|
||||||
|
|
||||||
|
bool showReviews = Model.Items.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -7,7 +11,7 @@
|
|||||||
<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 active" aria-current="page"><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 active" aria-current="page">Discovery</li>
|
<li class="breadcrumb-item active" aria-current="page">Discovery</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
@ -20,7 +24,7 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<section id="book-cover">
|
<section id="book-cover">
|
||||||
<div class="card character-card" id="cover-card">
|
<div class="card character-card" id="cover-card">
|
||||||
<responsive-image src="the-alpha-flame-11.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>
|
<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">
|
<div class="card-body border-top border-3 border-dark">
|
||||||
<h3 class="card-title">The Front Cover</h3>
|
<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>
|
<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>
|
||||||
@ -29,76 +33,238 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Synopsis Section -->
|
@if (showReviews)
|
||||||
<div class="col-md-8">
|
{
|
||||||
<section id="synopsis">
|
<!-- Buy Section -->
|
||||||
<div class="card character-card" id="synopsis-card">
|
<div class="col-md-8">
|
||||||
<div class="card-header">
|
<section id="purchase-and-reviews">
|
||||||
<h1>The Alpha Flame: <span class="fw-light">Discovery</span><br /><span class="h2">A Gritty 1980s Birmingham Crime Novel</span></h1>
|
<div class="card character-card" id="companion-card">
|
||||||
<h2 class="h3">Survival, secrets, and sisters in 1980s Birmingham.</h2>
|
<div class="card-header">
|
||||||
</div>
|
<h1>The Alpha Flame: <span class="fw-light">Discovery</span><br /><span class="h2">A Gritty 1980s Birmingham Crime Novel</span></h1>
|
||||||
<div class="card-body" id="synopsis-body">
|
<h2 class="h3">Survival, secrets, and sisters in 1980s Birmingham.</h2>
|
||||||
<div class="row align-items-center">
|
</div>
|
||||||
<div class="col-2">
|
<div class="card-body" id="companion-body">
|
||||||
<responsive-image src="catherine-lynwood-16.png" class="img-fluid rounded-circle border border-2 border-dark" alt="Catherine Lynwood" display-width-percentage="100"></responsive-image>
|
<div class="p-2">
|
||||||
|
<h2 class="h5">Buy the Book</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-10">
|
<!-- Buy Now Section -->
|
||||||
<!-- Audio Section -->
|
<div id="buy-now" class="mb-4">
|
||||||
<div class="audio-player text-center">
|
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" target="_blank" class="btn btn-dark mb-2">
|
||||||
<audio id="player">
|
Buy Kindle Edition
|
||||||
<source src="/audio/the-alpha-flame-discovery-synopsis.mp3" type="audio/mpeg">
|
</a>
|
||||||
Your browser does not support the audio element.
|
<a id="paperbackLink" href="https://www.amazon.co.uk/dp/1068225815" target="_blank" class="btn btn-dark mb-2">
|
||||||
</audio>
|
Buy Paperback (Bookshop Edition)
|
||||||
</div>
|
</a>
|
||||||
<p class="text-center text-muted small">
|
<a id="hardbackLink" href="https://www.amazon.co.uk/dp/1068225807" target="_blank" class="btn btn-dark mb-2">
|
||||||
Listen to Catherine telling you about The Alpha Flame: Discovery
|
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>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
<div class="row">
|
</div>
|
||||||
<div class="col-12">
|
</section>
|
||||||
<div id="buy-now" class="my-4">
|
</div>
|
||||||
<a id="kindleLink" href="https://www.amazon.com/dp/B0FBS427VD" target="_blank" class="btn btn-dark mb-2">
|
|
||||||
Buy Kindle Edition
|
|
||||||
</a>
|
<!-- Synopsis Section -->
|
||||||
<a id="paperbackLink" href="https://www.amazon.co.uk/dp/1068225815" target="_blank" class="btn btn-dark mb-2">
|
<div class="col-md-12">
|
||||||
Buy Paperback (Bookshop Edition)
|
<section id="synopsis">
|
||||||
</a>
|
<div class="card character-card">
|
||||||
<p id="geoNote" class="text-muted small mt-2">
|
<div class="card-header">
|
||||||
Available from your local Amazon store.<br />
|
<h2 class="card-title h1">The Alpha Flame: <span class="fw-light">Discovery:</span> Synopsis</h2>
|
||||||
Or order from your local bookshop using:
|
</div>
|
||||||
<ul class="small text-muted">
|
<div class="card-body" id="synopsis-body">
|
||||||
<li>
|
<div class="row align-items-center">
|
||||||
ISBN 978-1-0682258-1-9 - Bookshop Edition (Paperback)
|
<div class="col-2">
|
||||||
</li>
|
<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>
|
||||||
<li>
|
</div>
|
||||||
ISBN 978-1-0682258-0-2 - Collector's Eidtion (Hardback)
|
<div class="col-10">
|
||||||
</li>
|
<!-- Audio Section -->
|
||||||
</ul>
|
<div class="audio-player text-center">
|
||||||
<span id="extraRetailers"></span>
|
<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>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
</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 -->
|
||||||
<!-- Synopsis Content -->
|
@if (DateTime.Now < new DateTime(2025, 9, 1))
|
||||||
<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>
|
<div class="col-md-12 mt-4">
|
||||||
<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>
|
<div class="card">
|
||||||
<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>
|
<div class="card-body text-center">
|
||||||
<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>
|
<h2 class="display-6 fw-bold">Win: <span class="fw-light">a Collector’s Edition of The Alpha Flame: Discovery</span></h2>
|
||||||
<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="mb-2">
|
||||||
<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>
|
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 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>
|
</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>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chapter Previews Section -->
|
<!-- Chapter Previews Section -->
|
||||||
@ -149,17 +315,17 @@
|
|||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", function () {
|
window.addEventListener("load", function () {
|
||||||
const coverCard = document.getElementById("cover-card");
|
const coverCard = document.getElementById("cover-card");
|
||||||
const synopsisCard = document.getElementById("synopsis-card");
|
const companionCard = document.getElementById("companion-card");
|
||||||
const synopsisBody = document.getElementById("synopsis-body");
|
const companionBody = document.getElementById("companion-body");
|
||||||
|
|
||||||
if (coverCard && synopsisCard && synopsisBody) {
|
if (coverCard && companionCard && companionBody) {
|
||||||
// Match the height of the synopsis card to the cover card
|
// Match the height of the synopsis card to the cover card
|
||||||
const coverHeight = coverCard.offsetHeight;
|
const coverHeight = coverCard.offsetHeight;
|
||||||
synopsisCard.style.height = `${coverHeight}px`;
|
companionCard.style.height = `${coverHeight}px`;
|
||||||
|
|
||||||
// Adjust the synopsis body to scroll within the matched height
|
// Adjust the synopsis body to scroll within the matched height
|
||||||
const headerHeight = synopsisCard.querySelector(".card-header").offsetHeight;
|
const headerHeight = companionCard.querySelector(".card-header").offsetHeight;
|
||||||
synopsisBody.style.maxHeight = `${coverHeight - headerHeight}px`;
|
companionBody.style.maxHeight = `${coverHeight - headerHeight}px`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -176,37 +342,40 @@
|
|||||||
|
|
||||||
let kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
|
let kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
|
||||||
let paperbackLink = "https://www.amazon.com/dp/1068225815";
|
let paperbackLink = "https://www.amazon.com/dp/1068225815";
|
||||||
|
let hardbackLink = "https://www.amazon.com/dp/1068225807";
|
||||||
let extraRetailers = "";
|
let extraRetailers = "";
|
||||||
|
|
||||||
switch (country) {
|
switch (country) {
|
||||||
case "GB":
|
case "GB":
|
||||||
kindleLink = "https://www.amazon.co.uk/dp/B0FBS427VD";
|
kindleLink = "https://www.amazon.co.uk/dp/B0FBS427VD";
|
||||||
paperbackLink = "https://www.amazon.co.uk/dp/1068225815";
|
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>';
|
extraRetailers = 'Also available at <a href="https://www.waterstones.com/book/the-alpha-flame/catherine-lynwood/9781068225819" target="_blank">Waterstons</a>';
|
||||||
break;
|
break;
|
||||||
case "US":
|
case "US":
|
||||||
kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
|
kindleLink = "https://www.amazon.com/dp/B0FBS427VD";
|
||||||
paperbackLink = "https://www.amazon.com/dp/1068225815";
|
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>';
|
extraRetailers = 'Also available at <a href="https://www.barnesandnoble.com/s/9781068225810" target="_blank">Barnes & Noble</a>';
|
||||||
break;
|
break;
|
||||||
case "CA":
|
case "CA":
|
||||||
kindleLink = "https://www.amazon.ca/dp/B0FBS427VD";
|
kindleLink = "https://www.amazon.ca/dp/B0FBS427VD";
|
||||||
paperbackLink = "https://www.amazon.ca/dp/1068225815";
|
paperbackLink = "https://www.amazon.ca/dp/1068225815";
|
||||||
|
hardbackLink = "https://www.amazon.ca/dp/1068225807";
|
||||||
break;
|
break;
|
||||||
case "AU":
|
case "AU":
|
||||||
kindleLink = "https://www.amazon.com.au/dp/B0FBS427VD";
|
kindleLink = "https://www.amazon.com.au/dp/B0FBS427VD";
|
||||||
paperbackLink = "https://www.amazon.com.au/dp/1068225815";
|
paperbackLink = "https://www.amazon.com.au/dp/1068225815";
|
||||||
|
hardbackLink = "https://www.amazon.com.au/dp/1068225807";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("kindleLink").setAttribute("href", kindleLink);
|
document.getElementById("kindleLink").setAttribute("href", kindleLink);
|
||||||
document.getElementById("paperbackLink").setAttribute("href", paperbackLink);
|
document.getElementById("paperbackLink").setAttribute("href", paperbackLink);
|
||||||
|
document.getElementById("hardbackLink").setAttribute("href", hardbackLink);
|
||||||
document.getElementById("extraRetailers").innerHTML = extraRetailers;
|
document.getElementById("extraRetailers").innerHTML = extraRetailers;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -216,7 +385,7 @@
|
|||||||
meta-keywords="The Alpha Flame Discovery, Catherine Lynwood, 1983 novel, twin sisters, suspense fiction, Rubery, Birmingham fiction, historical drama, family secrets"
|
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-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||||
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-11-1200.webp"
|
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"
|
meta-image-alt="Maggie from 'The Alpha Flame: Discovery' by Catherine Lynwood"
|
||||||
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
|
og-site-name="Catherine Lynwood - The Alpha Flame: Discovery"
|
||||||
article-published-time="@new DateTime(2024, 11, 20)"
|
article-published-time="@new DateTime(2024, 11, 20)"
|
||||||
@ -225,86 +394,8 @@
|
|||||||
twitter-site-handle="@@CathLynwood"
|
twitter-site-handle="@@CathLynwood"
|
||||||
twitter-creator-handle="@@CathLynwood" />
|
twitter-creator-handle="@@CathLynwood" />
|
||||||
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
@Html.Raw(Model.SchemaJsonLd)
|
||||||
"@@context": "https://schema.org",
|
|
||||||
"@@type": "Book",
|
|
||||||
"name": "The Alpha Flame: Discovery",
|
|
||||||
"alternateName": "The Alpha Flame Book 1",
|
|
||||||
"image": "https://www.catherinelynwood.com/images/webp/the-alpha-flame-11-1200.webp",
|
|
||||||
"author": {
|
|
||||||
"@@type": "Person",
|
|
||||||
"name": "Catherine Lynwood",
|
|
||||||
"url": "https://www.catherinelynwood.com"
|
|
||||||
},
|
|
||||||
"publisher": {
|
|
||||||
"@@type": "Organization",
|
|
||||||
"name": "Catherine Lynwood Publishing",
|
|
||||||
"url": "https://www.catherinelynwood.com"
|
|
||||||
},
|
|
||||||
"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. As past traumas resurface and danger closes in, their journey through survival, sisterhood, and redemption begins.",
|
|
||||||
"genre": "Women's Fiction, Mystery, Contemporary Historical",
|
|
||||||
"inLanguage": "en-GB",
|
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery",
|
|
||||||
"workExample": [
|
|
||||||
{
|
|
||||||
"@@type": "Book",
|
|
||||||
"bookFormat": "https://schema.org/Hardcover",
|
|
||||||
"isbn": "978-1-0682258-0-2",
|
|
||||||
"name": "The Alpha Flame: Discovery – Hardback",
|
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery",
|
|
||||||
"offers": {
|
|
||||||
"@@type": "Offer",
|
|
||||||
"price": 23.99,
|
|
||||||
"priceCurrency": "GBP",
|
|
||||||
"availability": "https://schema.org/InStock"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@@type": "Book",
|
|
||||||
"bookFormat": "https://schema.org/Paperback",
|
|
||||||
"isbn": "978-1-0682258-1-9",
|
|
||||||
"name": "The Alpha Flame: Discovery – Paperback (Bookshop Edition)",
|
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery",
|
|
||||||
"offers": {
|
|
||||||
"@@type": "Offer",
|
|
||||||
"price": 17.99,
|
|
||||||
"priceCurrency": "GBP",
|
|
||||||
"availability": "https://schema.org/InStock"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@@type": "Book",
|
|
||||||
"bookFormat": "https://schema.org/Paperback",
|
|
||||||
"isbn": "978-1-0682258-2-6",
|
|
||||||
"name": "The Alpha Flame: Discovery – Amazon Edition",
|
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery",
|
|
||||||
"offers": {
|
|
||||||
"@@type": "Offer",
|
|
||||||
"price": 14.99,
|
|
||||||
"priceCurrency": "GBP",
|
|
||||||
"availability": "https://schema.org/InStock"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"@@type": "Book",
|
|
||||||
"bookFormat": "https://schema.org/EBook",
|
|
||||||
"isbn": "978-1-0682258-3-3",
|
|
||||||
"name": "The Alpha Flame: Discovery – eBook",
|
|
||||||
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery",
|
|
||||||
"offers": {
|
|
||||||
"@@type": "Offer",
|
|
||||||
"price": 3.95,
|
|
||||||
"priceCurrency": "GBP",
|
|
||||||
"availability": "https://schema.org/InStock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
const audioBlobSizes = new Map(); // id => size in bytes
|
const audioBlobSizes = new Map(); // id => size in bytes
|
||||||
let totalBlobSize = 0;
|
let totalBlobSize = 0;
|
||||||
const MAX_BLOB_CACHE_SIZE = 100 * 1024 * 1024; // 100 MB
|
const MAX_BLOB_CACHE_SIZE = 100 * 1024 * 1024; // 100 MB
|
||||||
const BUFFER_TARGET_SECONDS = 15 * 60; // 30 minutes
|
const BUFFER_TARGET_SECONDS = 30 * 60; // 30 minutes
|
||||||
|
|
||||||
let masterPlaylist = [];
|
let masterPlaylist = [];
|
||||||
let chapterStartIndices = [];
|
let chapterStartIndices = [];
|
||||||
@ -159,7 +159,9 @@
|
|||||||
let listHtml = '';
|
let listHtml = '';
|
||||||
const currentPos = chapterStartIndices.findIndex(c => c.name === currentChapter);
|
const currentPos = chapterStartIndices.findIndex(c => c.name === currentChapter);
|
||||||
const totalChapters = chapterStartIndices.length;
|
const totalChapters = chapterStartIndices.length;
|
||||||
const windowSize = 5;
|
|
||||||
|
// Dynamically determine window size based on screen width
|
||||||
|
const windowSize = getWindowSize();
|
||||||
const halfWindow = Math.floor(windowSize / 2);
|
const halfWindow = Math.floor(windowSize / 2);
|
||||||
|
|
||||||
// Previous button
|
// Previous button
|
||||||
@ -211,6 +213,23 @@
|
|||||||
document.getElementById("chapter-list").innerHTML = listHtml;
|
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) {
|
function goToPrevChapter(currentPos) {
|
||||||
if (currentPos <= 0) return;
|
if (currentPos <= 0) return;
|
||||||
|
|||||||
101
CatherineLynwood/Views/Discovery/Reviews.cshtml
Normal file
101
CatherineLynwood/Views/Discovery/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>
|
||||||
|
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<div class="float-md-start w-md-50 p-3">
|
<div class="float-md-start w-md-50 p-3">
|
||||||
<figure>
|
<figure>
|
||||||
<responsive-image src="catherine-lynwood-15.png" alt="Catherine Lynwood" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
<responsive-image src="catherine-lynwood-15.png" alt="Catherine Lynwood" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" display-width-percentage="50"></responsive-image>
|
||||||
<figcaption>Catherin Lynwood</figcaption>
|
<figcaption>Catherine Lynwood</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<header>
|
<header>
|
||||||
@ -37,9 +37,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p>It was only after years of juggling her design work and her love for storytelling that Catherine decided to take the plunge and begin writing her first novel. Drawing on her life experiences, her love for strong, complex characters, and the rich settings of her hometown, Catherine wrote <a asp-controller="TheAlphaFlame" asp-action="Index" class="link-dark fw-bold">The Alpha Flame</a> as a deeply personal exploration of resilience, mystery, and identity. Her transition from graphic design to the literary world has allowed her to combine her eye for visual storytelling with the depth of narrative, creating a unique voice that speaks to readers on many levels.</p>
|
<p>It was only after years of juggling her design work and her love for storytelling that Catherine decided to take the plunge and begin writing her first novel. Drawing on her life experiences, her love for strong, complex characters, and the rich settings of her hometown, Catherine wrote <a asp-controller="TheAlphaFlame" asp-action="Index" class="link-dark fw-bold">The Alpha Flame</a> as a deeply personal exploration of resilience, mystery, and identity. The novel helped define a new genre Catherine now refers to as <a asp-controller="Home" asp-action="VerosticGenre" class="link-dark fw-bold">Verostic fiction</a> — a style grounded in emotional realism, psychological depth, and unapologetic truth. <em>The Alpha Flame</em>, set in the emotional and cultural landscape of 1980s Britain, is also <strong>Aeverostic</strong> in tone — where the past is not just a backdrop, but a living, breathing part of the story’s truth.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p>Outside her creative career, Catherine’s life is deeply rooted in family. She lives just outside Birmingham with her daughter, who is her greatest inspiration and, often, her toughest critic. She describes her as having curious, quick-witted, and strong-willed—traits she values deeply and strives to cultivate in her own writing. Catherine finds joy in observing the unique perspectives her daughter brings to the world, and she often reminds her of the importance of resilience and self-discovery, themes central to her work.</p>
|
<p>Outside her creative career, Catherine’s life is deeply rooted in family. She lives just outside Birmingham with her daughter, who is her greatest inspiration and, often, her toughest critic. She describes her as having curious, quick-witted, and strong-willed—traits she values deeply and strives to cultivate in her own writing. Catherine finds joy in observing the unique perspectives her daughter brings to the world, and she often reminds her of the importance of resilience and self-discovery, themes central to her work.</p>
|
||||||
</section>
|
</section>
|
||||||
@ -48,9 +49,9 @@
|
|||||||
<p>Balancing family life with her career, Catherine believes in nurturing a creative household. She encourages her daughter <a asp-action="SamanthaLynwood" class="link-dark fw-bold">Samantha</a> to explore her passions, whether through art, music, or reading, hoping to instill in them a love for creativity and storytelling. Their family time often involves visits to museums, local theaters, or simply gathering around a table for sketching and brainstorming, making Catherine’s home a space where stories are shared and ideas take shape.</p>
|
<p>Balancing family life with her career, Catherine believes in nurturing a creative household. She encourages her daughter <a asp-action="SamanthaLynwood" class="link-dark fw-bold">Samantha</a> to explore her passions, whether through art, music, or reading, hoping to instill in them a love for creativity and storytelling. Their family time often involves visits to museums, local theaters, or simply gathering around a table for sketching and brainstorming, making Catherine’s home a space where stories are shared and ideas take shape.</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@section Meta {
|
@section Meta {
|
||||||
<meta name="description" content="Learn about Catherine Lynwood, author of *The Alpha Flame*, a compelling novel of romance, mystery, and family secrets. Discover Catherine’s inspirations, her journey as a novelist, and her commitment to stories featuring strong female characters and intricate plots.">
|
<meta name="description" content="Learn about Catherine Lynwood, author of *The Alpha Flame*, a compelling novel of romance, mystery, and family secrets. Discover Catherine’s inspirations, her journey as a novelist, and her commitment to stories featuring strong female characters and intricate plots.">
|
||||||
<meta name="keywords" content="Catherine Lynwood author, about Catherine Lynwood, Catherine Lynwood biography, women’s fiction author, mystery and romance novelist, family secrets book author, female protagonists in fiction, Catherine Lynwood novels, women’s novel writer, The Alpha Flame author, female fiction writers">
|
<meta name="keywords" content="Catherine Lynwood author, about Catherine Lynwood, Catherine Lynwood biography, women’s fiction author, mystery and romance novelist, family secrets book author, female protagonists in fiction, Catherine Lynwood novels, women’s novel writer, The Alpha Flame author, female fiction writers">
|
||||||
|
|||||||
344
CatherineLynwood/Views/Home/ArcReaderApplication.cshtml
Normal file
344
CatherineLynwood/Views/Home/ArcReaderApplication.cshtml
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
@model CatherineLynwood.Models.ArcReaderApplicationModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "ARC Reader Application";
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="container my-5">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-header bg-dark text-white">
|
||||||
|
<h2 class="mb-0">Catherine Lynwood ARC Reader Application</h2>
|
||||||
|
<p class="mb-0"><em>For <strong>The Alpha Flame: Discovery</strong> – Advance Reader Copy (ARC)</em></p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form asp-action="ArcReaderApplication" id="arcWizardForm" novalidate method="post">
|
||||||
|
<div class="progress mb-4" style="height: 1.5rem;">
|
||||||
|
<div id="arcWizardProgress" class="progress-bar bg-success" style="width: 25%;">
|
||||||
|
Step 1 of 5
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="arc-step" style="min-height: 65vh;" data-step="1">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<section class="arc-intro">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="alert alert-warning mt-4">
|
||||||
|
<i class="fad fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Note:</strong> This novel is raw and emotional. Please check the themes below.
|
||||||
|
</div>
|
||||||
|
<h2 class="h5 mt-4"><i class="fad fa-tags me-2 text-secondary"></i>Major Themes and Topics</h2>
|
||||||
|
<ul class="fa-ul">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<li><span class="fa-li"><i class="fad fa-procedures text-muted"></i></span> Mental health, including suicidal thoughts and PTSD</li>
|
||||||
|
<li><span class="fa-li"><i class="fad fa-money-bill-wave text-warning"></i></span> Prostitution, exploitation, and coercion</li>
|
||||||
|
<li><span class="fa-li"><i class="fad fa-bomb text-danger"></i></span> Violence against women (threats, assaults, murder)</li>
|
||||||
|
<li><span class="fa-li"><i class="fad fa-hand-holding-heart text-success"></i></span> Love, trust, tenderness, and emotional recovery</li>
|
||||||
|
<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><strong>All I ask:</strong> read the book and leave a review on Goodreads, Amazon, or wherever you normally post.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
||||||
|
</div>
|
||||||
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
So that I can address you in our communcations I need your name. I don't need your full name if you don't want to give it. Your first name or even a nickname will suffice.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="FullName"></label>
|
||||||
|
<input asp-for="FullName" class="form-control" />
|
||||||
|
<span asp-validation-for="FullName" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Email"></label>
|
||||||
|
<input asp-for="Email" class="form-control" />
|
||||||
|
<small class="form-text text-muted">This is where I’ll send updates and reminders.</small>
|
||||||
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
<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>Add the following sender to your Amazon Kindle Approved Senders list: <strong id="arc-email">[enable JavaScript to view]</strong></p>
|
||||||
|
<p>
|
||||||
|
To find your kindle email follow this link <a href="https://www.amazon.co.uk/myk" target="_blank">amazon.co.uk/myk</a>.
|
||||||
|
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 class="form-group">
|
||||||
|
<label asp-for="ApprovedSender"></label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="Yes" id="ApprovedYes" />
|
||||||
|
<label class="form-check-label" for="ApprovedYes">Yes — I’ve added your email</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="NotYet" id="ApprovedNotYet" />
|
||||||
|
<label class="form-check-label" for="ApprovedNotYet">Not yet — but I will</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ApprovedSender" value="NeedHelp" id="ApprovedNeedHelp" />
|
||||||
|
<label class="form-check-label" for="ApprovedNeedHelp">I need help</label>
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="ApprovedSender" class="text-danger"></span>
|
||||||
|
</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">
|
||||||
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Review Platforms -->
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
Please let me know what platforms you usually post your reviews on.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Platforms"></label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="Platforms" value="Amazon" id="PlatformAmazon" />
|
||||||
|
<label class="form-check-label" for="PlatformAmazon">Amazon</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="Platforms" value="Goodreads" id="PlatformGoodreads" />
|
||||||
|
<label class="form-check-label" for="PlatformGoodreads">Goodreads</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="Platforms" value="Instagram" id="PlatformInstagram" />
|
||||||
|
<label class="form-check-label" for="PlatformInstagram">Instagram</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="Platforms" value="TikTok" id="PlatformTikTok" />
|
||||||
|
<label class="form-check-label" for="PlatformTikTok">TikTok</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="Platforms" value="YourTube" id="PlatformYouTube" />
|
||||||
|
<label class="form-check-label" for="PlatformYouTube">YouTube</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="Platforms" value="Blog" id="PlatformBlog" />
|
||||||
|
<label class="form-check-label" for="PlatformBlog">Blog</label>
|
||||||
|
</div>
|
||||||
|
<label asp-for="PlatformsOther"></label>
|
||||||
|
<input class="form-control mt-2" asp-for="PlatformsOther" />
|
||||||
|
<span asp-validation-for="PlatformsOther" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Review Link -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ReviewLink"></label>
|
||||||
|
<input asp-for="ReviewLink" class="form-control" />
|
||||||
|
<span asp-validation-for="ReviewLink" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-3">
|
||||||
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
|
<button type="button" class="btn btn-primary arc-next">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="arc-step d-none" style="min-height: 65vh;" data-step="5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Content Fit -->
|
||||||
|
<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.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ContentFit"></label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ContentFit" value="Yes" id="ContentYes" />
|
||||||
|
<label class="form-check-label" for="ContentYes">Yes — I love that sort of book</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ContentFit" value="Maybe" id="ContentMaybe" />
|
||||||
|
<label class="form-check-label" for="ContentMaybe">Sounds interesting</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ContentFit" value="No" id="ContentNo" />
|
||||||
|
<label class="form-check-label" for="ContentNo">I prefer lighter reads</label>
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="ContentFit" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Chapters -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="PreviewChapters"></label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="PreviewChapters" value="1" id="Chapter1" />
|
||||||
|
<label class="form-check-label" for="Chapter1">Chapter 1</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="PreviewChapters" value="2" id="Chapter2" />
|
||||||
|
<label class="form-check-label" for="Chapter2">Chapter 2</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="PreviewChapters" value="13" id="Chapter13" />
|
||||||
|
<label class="form-check-label" for="Chapter13">Chapter 13</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Review Commitment -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ReviewCommitment"></label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ReviewCommitment" value="Yes" id="ReviewYes" />
|
||||||
|
<label class="form-check-label" for="ReviewYes">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ReviewCommitment" value="Try" id="ReviewTry" />
|
||||||
|
<label class="form-check-label" for="ReviewTry">I’ll try my best</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" asp-for="ReviewCommitment" value="No" id="ReviewNo" />
|
||||||
|
<label class="form-check-label" for="ReviewNo">Probably not</label>
|
||||||
|
</div>
|
||||||
|
<span asp-validation-for="ReviewCommitment" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Extra Notes -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ExtraNotes"></label>
|
||||||
|
<textarea asp-for="ExtraNotes" class="form-control" rows="4" placeholder="How you found me, favourite genres, etc."></textarea>
|
||||||
|
<span asp-validation-for="ExtraNotes" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
<strong>Privacy note:</strong> Your details will only be used for ARC-related communication and Kindle delivery. You can opt out at any time.
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary arc-back">Back</button>
|
||||||
|
<button type="submit" class="btn btn-success">Submit Application</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@section Scripts{
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const user = "catherine";
|
||||||
|
const domain = "catherinelynwood.com";
|
||||||
|
document.getElementById("arc-email").textContent = user + "@@" + domain;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const steps = document.querySelectorAll('.arc-step');
|
||||||
|
const progress = document.getElementById('arcWizardProgress');
|
||||||
|
const form = document.getElementById('arcWizardForm');
|
||||||
|
let currentStep = 1;
|
||||||
|
|
||||||
|
function showStep(step) {
|
||||||
|
steps.forEach(s => s.classList.add('d-none'));
|
||||||
|
const stepEl = document.querySelector(`.arc-step[data-step="${step}"]`);
|
||||||
|
if (stepEl) stepEl.classList.remove('d-none');
|
||||||
|
|
||||||
|
const percent = (step / steps.length) * 100;
|
||||||
|
if (progress) {
|
||||||
|
progress.style.width = percent + '%';
|
||||||
|
progress.textContent = `Step ${step} of ${steps.length}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCurrentStep() {
|
||||||
|
const currentFields = document.querySelector(`.arc-step[data-step="${currentStep}"]`);
|
||||||
|
const inputs = currentFields.querySelectorAll('input, select, textarea');
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
// This uses jQuery Unobtrusive Validation to validate Razor-bound fields
|
||||||
|
if (!$(input).valid()) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.querySelectorAll('.arc-next').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (validateCurrentStep()) {
|
||||||
|
currentStep++;
|
||||||
|
showStep(currentStep);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.arc-back').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
currentStep--;
|
||||||
|
showStep(currentStep);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Try to find first step with invalid field
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
const stepFields = steps[i].querySelectorAll('input, select, textarea');
|
||||||
|
for (const field of stepFields) {
|
||||||
|
if (!field.checkValidity()) {
|
||||||
|
currentStep = parseInt(steps[i].dataset.step);
|
||||||
|
showStep(currentStep);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
showStep(currentStep);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -17,16 +17,12 @@
|
|||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<form asp-action="ContactCatherine" class="row">
|
<form asp-action="ContactCatherine" method="post" onsubmit="disableSubmit(this)">
|
||||||
<div class="col-12 col-sm-8 col-md-12">
|
<div class="col-12 col-sm-8 col-md-12">
|
||||||
<h1>Contact Catherine</h1>
|
<h1>Contact Catherine</h1>
|
||||||
<p>I would love to hear your thoughts regarding my work. Please feel free to contact me and I will try my best to reply as soon as I can.</p>
|
<p>I would love to hear your thoughts regarding my work. Please feel free to contact me and I will try my best to reply as soon as I can.</p>
|
||||||
<p>Use the form below to send Catherine a message.</p>
|
<p>Use the form below to send Catherine a message.</p>
|
||||||
</div>
|
</div>
|
||||||
@if (ViewData["VpnWarning"] is true)
|
|
||||||
{
|
|
||||||
<partial name="_VPNWarning" />
|
|
||||||
}
|
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
<div class="col-12 text-dark">
|
<div class="col-12 text-dark">
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
@ -46,15 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
@if (ViewData["VpnWarning"] is true)
|
<button type="submit" class="btn btn-dark btn-block mb-3">Send Message</button>
|
||||||
{
|
|
||||||
<button type="submit" class="btn btn-dark btn-block mb-3" disabled>Send Message</button>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<button type="submit" class="btn btn-dark btn-block mb-3">Send Message</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -75,3 +63,16 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Scripts{
|
||||||
|
<script>
|
||||||
|
function disableSubmit(form) {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = 'Sending...'; // Optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Collaboration Inquiry";
|
ViewData["Title"] = "Collaboration Inquiry";
|
||||||
}
|
}
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-10 offset-md-1">
|
<div class="col-md-10 offset-md-1">
|
||||||
@ -39,3 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Meta{
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
}
|
||||||
@ -43,13 +43,22 @@
|
|||||||
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
|
<a asp-controller="Discovery" asp-action="Extras" class="btn btn-outline-dark">Unlock Extras</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
<em>The Alpha Flame: Discovery</em>, 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="h6 d-inline"><em>The Alpha Flame: Discovery</em></h2>, writen by <a asp-controller="Home" asp-action="AboutCatherineLynwood" class="link-dark fw-semibold">Catherine Lynwood</a>, is the first in a powerful trilogy following the tangled lives of Maggie and Beth, two women bound by fate, fire, and secrets too dangerous to stay buried.
|
||||||
The journey continues in <strong>Reckoning</strong> (Spring 2026) and concludes with <strong>Redemption</strong> (Autumn 2026).
|
The journey continues in <strong>Reckoning</strong> (Spring 2026) and concludes with <strong>Redemption</strong> (Autumn 2026).
|
||||||
Learn more about the full trilogy on the <a asp-controller="TheAlphaFlame" asp-action="Index" class="link-dark fw-semibold">Alpha Flame series page</a>.
|
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>.
|
||||||
</p>
|
</p>
|
||||||
|
@if (DateTime.Now < new DateTime(2025, 9, 1))
|
||||||
|
{
|
||||||
|
<p class="mt-4">
|
||||||
|
<h2 class="display-6 fw-bold">Win: <span class="fw-light">a Collector’s Edition of The Alpha Flame: Discovery</span></h2>
|
||||||
|
<em>Exclusive, limited, beautiful.</em>
|
||||||
|
</p>
|
||||||
|
<div class="d-flex gap-3 flex-wrap mt-4">
|
||||||
|
<a asp-controller="TheAlphaFlame" asp-action="Giveaways" class="btn btn-dark">Enter now for your chance.</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
<h3 class="h4">About The Alpha Flame Trilogy</h3>
|
<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.
|
||||||
Catherine Lynwood’s <em>The Alpha Flame trilogy</em> 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.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -62,7 +71,7 @@
|
|||||||
meta-keywords="Catherine Lynwood, The Alpha Flame, historical fiction, 1983 novel, Birmingham author, twin sisters, suspense trilogy, female-led fiction"
|
meta-keywords="Catherine Lynwood, The Alpha Flame, historical fiction, 1983 novel, Birmingham author, twin sisters, suspense trilogy, female-led fiction"
|
||||||
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-11-600.webp"
|
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-600.webp"
|
||||||
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)"
|
||||||
@ -101,6 +110,7 @@
|
|||||||
"@@type": "Book",
|
"@@type": "Book",
|
||||||
"name": "The Alpha Flame: Discovery",
|
"name": "The Alpha Flame: Discovery",
|
||||||
"alternateName": "The Alpha Flame Book 1",
|
"alternateName": "The Alpha Flame Book 1",
|
||||||
|
"image": "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||||
"author": {
|
"author": {
|
||||||
"@@type": "Person",
|
"@@type": "Person",
|
||||||
"name": "Catherine Lynwood",
|
"name": "Catherine Lynwood",
|
||||||
@ -120,32 +130,62 @@
|
|||||||
"@@type": "Book",
|
"@@type": "Book",
|
||||||
"bookFormat": "https://schema.org/Hardcover",
|
"bookFormat": "https://schema.org/Hardcover",
|
||||||
"isbn": "978-1-0682258-0-2",
|
"isbn": "978-1-0682258-0-2",
|
||||||
"name": "The Alpha Flame: Discovery – Hardback"
|
"name": "The Alpha Flame: Discovery – Collector's Edition",
|
||||||
|
"image": "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||||
|
"offers": {
|
||||||
|
"@@type": "Offer",
|
||||||
|
"price": "23.99",
|
||||||
|
"priceCurrency": "GBP",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@@type": "Book",
|
"@@type": "Book",
|
||||||
"bookFormat": "https://schema.org/Paperback",
|
"bookFormat": "https://schema.org/Paperback",
|
||||||
"isbn": "978-1-0682258-1-9",
|
"isbn": "978-1-0682258-1-9",
|
||||||
"name": "The Alpha Flame: Discovery – Softback"
|
"name": "The Alpha Flame: Discovery – Bookshop Edition",
|
||||||
|
"image": "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||||
|
"offers": {
|
||||||
|
"@@type": "Offer",
|
||||||
|
"price": "17.99",
|
||||||
|
"priceCurrency": "GBP",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@@type": "Book",
|
"@@type": "Book",
|
||||||
"bookFormat": "https://schema.org/Paperback",
|
"bookFormat": "https://schema.org/Paperback",
|
||||||
"isbn": "978-1-0682258-2-6",
|
"isbn": "978-1-0682258-2-6",
|
||||||
"name": "The Alpha Flame: Discovery – Amazon Edition"
|
"name": "The Alpha Flame: Discovery – Amazon Edition",
|
||||||
|
"image": "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||||
|
"offers": {
|
||||||
|
"@@type": "Offer",
|
||||||
|
"price": "13.99",
|
||||||
|
"priceCurrency": "GBP",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@@type": "Book",
|
"@@type": "Book",
|
||||||
"bookFormat": "https://schema.org/EBook",
|
"bookFormat": "https://schema.org/EBook",
|
||||||
"isbn": "978-1-0682258-3-3",
|
"isbn": "978-1-0682258-3-3",
|
||||||
"name": "The Alpha Flame: Discovery – eBook"
|
"name": "The Alpha Flame: Discovery – eBook",
|
||||||
|
"image": "https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-cover-1200.webp",
|
||||||
|
"offers": {
|
||||||
|
"@@type": "Offer",
|
||||||
|
"price": "3.95",
|
||||||
|
"priceCurrency": "GBP",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"url": "https://www.catherinelynwood.com/the-alpha-flame/discovery"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@ -57,3 +57,20 @@
|
|||||||
<a href="https://www.gov.uk/data-protection" target="_blank" rel="noopener noreferrer">www.gov.uk/data-protection</a>.
|
<a href="https://www.gov.uk/data-protection" target="_blank" rel="noopener noreferrer">www.gov.uk/data-protection</a>.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@section Meta{
|
||||||
|
<MetaTag meta-title="Privacy Policy – Catherine Lynwood Author Website"
|
||||||
|
meta-description="Read the privacy policy for Catherine Lynwood’s official author website. Learn how visitor data is collected, stored, and used responsibly."
|
||||||
|
meta-keywords="Catherine Lynwood privacy policy, data protection, author website privacy, GDPR compliance, visitor data"
|
||||||
|
meta-author="Catherine Lynwood"
|
||||||
|
meta-url="https://www.catherinelynwood.com/privacy"
|
||||||
|
meta-image="https://www.catherinelynwood.com/images/catherine-lynwood-logo.png"
|
||||||
|
meta-image-alt="Catherine Lynwood Author Logo"
|
||||||
|
og-site-name="Catherine Lynwood – Privacy Policy"
|
||||||
|
article-published-time="@new DateTime(2024, 11, 20)"
|
||||||
|
article-modified-time="@new DateTime(2025, 06, 17)"
|
||||||
|
twitter-card-type="summary_large_image"
|
||||||
|
twitter-site-handle="@@CathLynwood"
|
||||||
|
twitter-creator-handle="@@CathLynwood" />
|
||||||
|
|
||||||
|
}
|
||||||
221
CatherineLynwood/Views/Home/VerosticGenre.cshtml
Normal file
221
CatherineLynwood/Views/Home/VerosticGenre.cshtml
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "The Verostic Genre";
|
||||||
|
}
|
||||||
|
|
||||||
|
<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">The Verostic Genre</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="container py-2">
|
||||||
|
<h1 class="display-4 mb-4">The Verostic Genre</h1>
|
||||||
|
|
||||||
|
<!-- Dictionary Definition -->
|
||||||
|
<div class="mb-4 border-start border-4 border-dark ps-3">
|
||||||
|
<h2 class="h5 text-muted mb-1"><strong>verostic</strong> /ˈvɛr.ɒst.ɪk/ <em>adj.</em></h2>
|
||||||
|
<p class="mb-1"><strong>Origin:</strong> Latin <em>veritas</em> (truth) + stylistic suffix</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
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. The hallmark of a story that bleeds... and still finds beauty.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Genre Definition -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<h2 class="h4 text-muted">Definition</h2>
|
||||||
|
<p class="lead">
|
||||||
|
<strong>Verostic fiction</strong> is a genre defined by emotional rawness, psychological intensity, and unfiltered human truth.
|
||||||
|
It explores trauma, morality, resilience, and connection through unapologetically honest storytelling.
|
||||||
|
Gritty, intimate, and unflinching, Verostic works don't shy away from darkness... yet always make space for tenderness, hope, and truth.
|
||||||
|
</p>
|
||||||
|
<blockquote>
|
||||||
|
<p class="mb-0">Verostic fiction doesn’t just tell the truth, it makes you feel it.</p>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container overflow-hidden mb-5">
|
||||||
|
<div class="row gy-5">
|
||||||
|
<div class="col-md-4 border-start border-4 border-dark">
|
||||||
|
<div class="badge bg-dark">Neoverostic</div>
|
||||||
|
<h3 class="mt-4">Neoverostic (adj.)</h3>
|
||||||
|
<p><strong>From</strong> <em>neo</em> (Greek: new) + Verostic</p>
|
||||||
|
<p>
|
||||||
|
<strong>Definition:</strong><br>
|
||||||
|
Neoverostic fiction applies Verostic principles to modern contexts, exploring contemporary trauma, digital identity, intersectionality, and generational tension with the same emotional honesty. It often experiments with form, voice, or structure to reflect fractured lives or shifting perspectives.
|
||||||
|
</p>
|
||||||
|
<p>Expect raw emotion filtered through today's chaos: fractured families, economic precarity, gender politics, mental health, loneliness in the age of connection.</p>
|
||||||
|
<p><strong>Neoverostic stories are razor-sharp and present-tense, burning with modern truth.</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 border-start border-4 border-dark">
|
||||||
|
<div class="badge bg-dark">Postverostic</div>
|
||||||
|
<h3 class="mt-4">Postverostic (adj.)</h3>
|
||||||
|
<p><strong>From</strong> <em>post</em> (Latin: after) + Verostic</p>
|
||||||
|
<p>
|
||||||
|
<strong>Definition:</strong><br>
|
||||||
|
Postverostic fiction interrogates or deconstructs the Verostic mode. Often reflective, ironic, or stylistically fragmented, it explores what happens after trauma, truth, or confrontation, the silences, the scars, the reconstruction of meaning.
|
||||||
|
</p>
|
||||||
|
<p>These stories may feature unreliable narrators, metafictional layers, or broken timelines. They don’t abandon emotional realism but question how memory, narrative, and identity distort it.</p>
|
||||||
|
<p><strong>Postverostic fiction doesn’t just explore the truth, it wonders whether truth survives the telling.</strong></p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 border-start border-4 border-dark">
|
||||||
|
<div class="badge bg-dark">Aeverostic</div>
|
||||||
|
<h3 class="mt-4">Aeverostic (adj.)</h3>
|
||||||
|
<p><strong>From</strong> <em>aevum</em> (Latin: era, lifetime) + Verostic</p>
|
||||||
|
<p>
|
||||||
|
<strong>Definition:</strong><br>
|
||||||
|
Aeverostic fiction is a time-rooted subgenre of Verostic storytelling, most often set in the 1980s. It captures emotionally raw narratives within a specific cultural and historical moment, where truth is shaped by the era’s unique pressures: repression, rebellion, subculture, class division, family shame, and gender expectation.
|
||||||
|
</p>
|
||||||
|
<p>The term is not nostalgic. Aeverostic fiction does not look back fondly, it digs into the past with clarity and courage, reanimating the emotional texture of a generation.</p>
|
||||||
|
<p><strong>Aeverostic stories live and bleed in their time, but their truths still echo today.</strong></p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p class="mt-4">
|
||||||
|
<strong><a href="/the-alpha-flame/discovery">Read The Alpha Flame: Discovery</a></strong>, a work of unapologetically Verostic fiction, and feel the fire for yourself.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manifesto -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<h2 class="h4 text-muted">The Verostic Manifesto</h2>
|
||||||
|
<div class="accordion" id="verosticManifesto">
|
||||||
|
@{
|
||||||
|
var principles = new[]
|
||||||
|
{
|
||||||
|
"Verostic fiction is truth, undressed.",
|
||||||
|
"Verostic fiction bleeds.",
|
||||||
|
"Verostic fiction is character-first.",
|
||||||
|
"Verostic fiction isn’t loyal to genre, only to truth.",
|
||||||
|
"Verostic fiction always makes space for softness.",
|
||||||
|
"Verostic fiction evolves."
|
||||||
|
};
|
||||||
|
|
||||||
|
var descriptions = new[]
|
||||||
|
{
|
||||||
|
"It doesn’t flinch, doesn’t tidy things up, doesn’t protect you from pain. It trusts the reader to witness the mess, and find meaning in it.",
|
||||||
|
"It’s emotionally intense, sometimes harrowing, but never gratuitous. Its power lies in the tension between raw exposure and quiet humanity.",
|
||||||
|
"Plot follows truth. The story bends around the emotional journey, not the other way around.",
|
||||||
|
"You’ll find crime, romance, psychological twists, even moments of joy. But these are tools, not rules. Truth decides the shape.",
|
||||||
|
"Amid trauma, there is tenderness. Amid fear, hope. Amid silence, connection. Verostic fiction doesn’t just show wounds... it shows how people live with them.",
|
||||||
|
"It can be modern, reflective, or rooted in time. Whether it's today’s truth, yesterday’s echo, or tomorrow’s reckoning... the soul is the same."
|
||||||
|
};
|
||||||
|
@for (int i = 0; i < principles.Length; i++)
|
||||||
|
{
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="heading@(i)">
|
||||||
|
<button class="accordion-button @((i > 0 ? "collapsed" : ""))"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#collapse@(i)"
|
||||||
|
aria-expanded="@(i == 0 ? "true" : "false")"
|
||||||
|
aria-controls="collapse@(i)">
|
||||||
|
<strong>@principles[i]</strong>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapse@(i)"
|
||||||
|
class="accordion-collapse collapse @(i == 0 ? "show" : "")"
|
||||||
|
aria-labelledby="heading@(i)"
|
||||||
|
data-bs-parent="#verosticManifesto">
|
||||||
|
<div class="accordion-body">
|
||||||
|
@descriptions[i]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Genre Diagram -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<h2 class="h4 text-muted text-center">Visual Genre Chart</h2>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center align-items-center flex-column my-4">
|
||||||
|
<div class="fw-bold fs-3">Verostic</div>
|
||||||
|
<div class="d-flex flex-row justify-content-center align-items-start mt-3 genre-branches">
|
||||||
|
<div class="text-center mx-3">
|
||||||
|
<div class="badge bg-dark mb-2">Neoverostic</div>
|
||||||
|
<p class="small text-muted">Modern emotional realism, digital identity, intersectional trauma</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mx-3">
|
||||||
|
<div class="badge bg-dark mb-2">Postverostic</div>
|
||||||
|
<p class="small text-muted">Reflective, fragmented, aftermath of truth</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mx-3">
|
||||||
|
<div class="badge bg-dark mb-2">Aeverostic</div>
|
||||||
|
<p class="small text-muted">Era-rooted (1980s), cultural grit, timeless emotional truth</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-muted fst-italic text-center">
|
||||||
|
<blockquote class="blockquote">
|
||||||
|
<p class="mb-0">Verostic isn’t a style. It’s a vow.</p>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@section Meta{
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "WebPage",
|
||||||
|
"name": "The Verostic Genre",
|
||||||
|
"url": "https://www.catherinelynwood.com/verostic-genre",
|
||||||
|
"description": "A literary genre defined by emotional rawness, psychological depth, and unflinching truth. Includes the Verostic manifesto, genre definition, and visual subgenre chart.",
|
||||||
|
"mainEntity": {
|
||||||
|
"@@type": "DefinedTerm",
|
||||||
|
"name": "Verostic",
|
||||||
|
"termCode": "verostic-fiction",
|
||||||
|
"inDefinedTermSet": "https://www.catherinelynwood.com/verostic-genre",
|
||||||
|
"description": "A genre of fiction marked by gritty emotional realism, psychological intensity, and unapologetic storytelling. Verostic fiction explores trauma, morality, and human resilience through unfiltered emotional truth.",
|
||||||
|
"alternateName": [
|
||||||
|
"Verostic fiction",
|
||||||
|
"Grit-lit with soul",
|
||||||
|
"Emotionally raw psychological realism"
|
||||||
|
],
|
||||||
|
"hasDefinedTerm": [
|
||||||
|
{
|
||||||
|
"@@type": "DefinedTerm",
|
||||||
|
"name": "Neoverostic",
|
||||||
|
"description": "Modern Verostic fiction dealing with contemporary trauma, digital identity, and intersectional emotional truth.",
|
||||||
|
"termCode": "neoverostic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "DefinedTerm",
|
||||||
|
"name": "Postverostic",
|
||||||
|
"description": "Reflective or deconstructed Verostic fiction that explores aftermath, ambiguity, and memory distortion.",
|
||||||
|
"termCode": "postverostic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@@type": "DefinedTerm",
|
||||||
|
"name": "Aeverostic",
|
||||||
|
"description": "Era-specific Verostic fiction rooted in the emotional and cultural landscape of the 1980s.",
|
||||||
|
"termCode": "aeverostic"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
}
|
||||||
@ -73,6 +73,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Meta{
|
@section Meta{
|
||||||
|
<MetaTag meta-title="Publishing with Catherine Lynwood – The Alpha Flame Trilogy"
|
||||||
|
meta-description="Learn about Catherine Lynwood’s publishing journey for *The Alpha Flame Trilogy*, including distribution partners, formats, and exclusive editions available to readers."
|
||||||
|
meta-keywords="Catherine Lynwood publishing, The Alpha Flame Trilogy, indie author, book distribution, IngramSpark, Amazon KDP, Waterstones, hardback editions"
|
||||||
|
meta-author="Catherine Lynwood"
|
||||||
|
meta-url="https://www.catherinelynwood.com/publishing"
|
||||||
|
meta-image="https://www.catherinelynwood.com/images/catherine-lynwood-logo.png"
|
||||||
|
meta-image-alt="Publishing details for Catherine Lynwood’s The Alpha Flame Trilogy"
|
||||||
|
og-site-name="Catherine Lynwood – The Alpha Flame Publishing"
|
||||||
|
article-published-time="@new DateTime(2024, 11, 20)"
|
||||||
|
article-modified-time="@new DateTime(2025, 06, 17)"
|
||||||
|
twitter-card-type="summary_large_image"
|
||||||
|
twitter-site-handle="@@CathLynwood"
|
||||||
|
twitter-creator-handle="@@CathLynwood" />
|
||||||
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@@context": "https://schema.org",
|
"@@context": "https://schema.org",
|
||||||
|
|||||||
@ -43,17 +43,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<form asp-action="Comment" class="row" id="comment">
|
<form asp-action="Comment" class="row" id="comment" onsubmit="disableSubmit(this)">
|
||||||
<div class="col-12 pb-3">
|
<div class="col-12 pb-3">
|
||||||
<h2>Leave a Comment</h2>
|
<h2>Leave a Comment</h2>
|
||||||
In invite you to leave comments regarding this blog post. As you can see I ask that you give me your name and email address, as well as your age and sex.
|
In invite you to leave comments regarding this blog post. As you can see I ask that you give me your name and email address, as well as your age and sex.
|
||||||
This is so I can better understand my audience and tailor my content to suit your needs. I will never share your information with anyone else. I look forward to hearing from you.
|
This is so I can better understand my audience and tailor my content to suit your needs. I will never share your information with anyone else. I look forward to hearing from you.
|
||||||
Please note that I may email you and ask for a reply before publishing your comment. This is to ensure that I am not publishing spam comments.
|
Please note that I may email you and ask for a reply before publishing your comment. This is to ensure that I am not publishing spam comments.
|
||||||
</div>
|
</div>
|
||||||
@if (ViewData["VpnWarning"] is true)
|
|
||||||
{
|
|
||||||
<partial name="_VPNWarning" />
|
|
||||||
}
|
|
||||||
<div class="col-12 text-dark">
|
<div class="col-12 text-dark">
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,16 +123,8 @@
|
|||||||
|
|
||||||
<!-- Placeholder Button and Submit Button -->
|
<!-- Placeholder Button and Submit Button -->
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
@if (ViewData["VpnWarning"] is true)
|
<button type="button" class="btn btn-secondary mb-3" id="fakeSubmitButton">Post Comment</button>
|
||||||
{
|
<button type="submit" class="btn btn-dark mb-3 d-none" id="submitButton">Post Comment</button>
|
||||||
<button type="button" class="btn btn-secondary mb-3" disabled>Post Comment</button>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<button type="button" class="btn btn-secondary mb-3" id="fakeSubmitButton">Post Comment</button>
|
|
||||||
<button type="submit" class="btn btn-dark mb-3 d-none" id="submitButton">Post Comment</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden Fields -->
|
<!-- Hidden Fields -->
|
||||||
|
|||||||
@ -46,7 +46,8 @@
|
|||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="Catherine Lynwood Blog Feed" href="https://www.catherinelynwood.com/feed">
|
<link rel="alternate" type="application/rss+xml" title="Catherine Lynwood Blog Feed" href="https://www.catherinelynwood.com/feed">
|
||||||
|
|
||||||
<link rel="canonical" href="@($"https://www.catherinelynwood.com{Context.Request.Path}{Context.Request.QueryString}")" />
|
|
||||||
|
<link rel="canonical" href="@($"https://www.catherinelynwood.com{Context.Request.Path}")" />
|
||||||
|
|
||||||
@RenderSection("Meta", required: false)
|
@RenderSection("Meta", required: false)
|
||||||
|
|
||||||
@ -86,26 +87,42 @@
|
|||||||
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="AboutCatherineLynwood">About Catherine</a>
|
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="AboutCatherineLynwood">About Catherine</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-primary" asp-area="" asp-controller="TheAlphaFlame" asp-action="Blog">Blog</a>
|
<a class="nav-link text-primary" asp-area="" asp-controller="TheAlphaFlame" asp-action="Blog">A Cuppa With Catherine</a>
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-primary" asp-area="" asp-controller="AskAQuestion" asp-action="Index">Ask Catherine</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle text-primary" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle text-primary" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
The Alpha Flame
|
The Alpha Flame
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li class="py-2"><a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Index">The Trilogy</a></li>
|
<li class="py-2">
|
||||||
@* <li><hr class="dropdown-divider"></li> *@
|
<a class="dropdown-item" asp-controller="Discovery" asp-action="Index">Discovery (Book 1)</a>
|
||||||
<li class="py-2"><a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Characters">Meet the Characters</a></li>
|
</li>
|
||||||
|
<li class="py-2">
|
||||||
|
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Index">The Trilogy</a>
|
||||||
|
</li>
|
||||||
|
<li class="py-2">
|
||||||
|
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Characters">Meet the Characters</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="ContactCatherine">Contact Catherine</a>
|
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="ArcReaderApplication">ARC Reader Application</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="VerosticGenre">The Verostic Genre</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-light" asp-controller="Admin" asp-action="Index" rel="nofollow" title="Admin">
|
||||||
|
<i class="fad fa-user-shield fa-lg"></i>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
@model CatherineLynwood.Models.BlogIndex
|
@model CatherineLynwood.Models.BlogIndex
|
||||||
|
@using Humanizer
|
||||||
|
@using CatherineLynwood.Helpers
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Catherine's Blog";
|
ViewData["Title"] = "A Cuppa With Catherine";
|
||||||
}
|
}
|
||||||
|
|
||||||
@* <style>
|
@* <style>
|
||||||
@ -25,31 +26,39 @@
|
|||||||
<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 active" aria-current="page">The Alpha Flame Blog</li>
|
<li class="breadcrumb-item active" aria-current="page">A Cuppa With Catherine</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row my-4">
|
||||||
<div class="col-12">
|
<div class="col-12 text-center">
|
||||||
<h1>The Alpha Flame Blog</h1>
|
<h1 class="display-4">
|
||||||
<p>Follow along as Catherine Lynwood shares updates, reflections, and glimpses into the worlds and characters she is passionate about bringing to life.</p>
|
<i class="fad fa-coffee" style="--fa-primary-color: #ffffff; --fa-secondary-color: #042830;"></i>
|
||||||
|
A Cuppa With Catherine
|
||||||
|
</h1>
|
||||||
|
<p class="lead mt-3 px-3">
|
||||||
|
Pop the kettle on and settle in for a cuppa. Catherine Lynwood invites you to share in her updates, reflections, and the worlds and characters she brings to life—like old friends having a natter over tea.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form asp-action="Blog" method="get" class="row border border-1 rounded-3 bg-white mx-0 mb-3">
|
|
||||||
|
<form method="get" class="row border border-1 rounded-3 bg-white mx-0 mb-3">
|
||||||
<div class="col-12 pt-3">
|
<div class="col-12 pt-3">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-2 col-md-10 col-lg-9 col-xl-8 col-xxl-7">
|
<div class="col-md-2 col-md-10 col-lg-9 col-xl-8 col-xxl-7">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<span class="input-group-text d-none d-sm-inline" id="basic-addon1">Sort by</span>
|
<span class="input-group-text d-none d-sm-inline">Sort by</span>
|
||||||
|
|
||||||
<select asp-for="BlogFilter.SortDirection" class="form-select js-submit-on-change" name="SortDirection" aria-label="Sort direction">
|
<select asp-for="BlogFilter.SortDirection" class="form-select js-submit-on-change" aria-label="Sort direction">
|
||||||
<option value="1">Newest first</option>
|
<option value="1">Newest first</option>
|
||||||
<option value="2">Oldest first</option>
|
<option value="2">Oldest first</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="input-group-text d-none d-sm-inline" id="basic-addon1">Results per page</span>
|
|
||||||
<select asp-for="BlogFilter.ResultsPerPage" class="form-select js-submit-on-change" name="ResultsPerPage" aria-label="Results per page">
|
<span class="input-group-text d-none d-sm-inline">Results per page</span>
|
||||||
|
|
||||||
|
<select asp-for="BlogFilter.ResultsPerPage" class="form-select js-submit-on-change" aria-label="Results per page">
|
||||||
<option value="6">6</option>
|
<option value="6">6</option>
|
||||||
<option value="12">12</option>
|
<option value="12">12</option>
|
||||||
<option value="18">18</option>
|
<option value="18">18</option>
|
||||||
@ -57,69 +66,31 @@
|
|||||||
<option value="30">30</option>
|
<option value="30">30</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button class="btn btn-dark" type="button" data-bs-toggle="collapse" data-bs-target="#categories">Show Categories</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden field for PageNumber -->
|
|
||||||
<input type="hidden" asp-for="BlogFilter.PageNumber" value="@Model.BlogFilter.PageNumber" name="PageNumber" id="PageNumber" />
|
|
||||||
<input type="hidden" asp-for="BlogFilter.TotalPages" value="@Model.BlogFilter.TotalPages" name="PreviousTotalPages" />
|
|
||||||
|
|
||||||
<div class="col-12 @(Model.ShowAdvanced ? "collapse show" : "collapse")" id="categories">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-10">
|
|
||||||
<ul class="list-inline">
|
|
||||||
@foreach (var item in Model.BlogCategories)
|
|
||||||
{
|
|
||||||
<li class="list-inline-item p-1">
|
|
||||||
<div class="form-check form-switch text-dark">
|
|
||||||
<input class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
id="switch-@item.CategoryID"
|
|
||||||
name="Categories"
|
|
||||||
value="@item.CategoryID"
|
|
||||||
checked="@item.Selected">
|
|
||||||
<label class="form-check-label" for="switch-@item.CategoryID">
|
|
||||||
@item.Category
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<button class="btn btn-dark w-100" type="submit">Apply Category Filter</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@if (Model.BlogFilter.TotalPages > 1)
|
@if (Model.BlogFilter.TotalPages > 1)
|
||||||
{
|
{
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<!-- Pagination Component -->
|
|
||||||
<nav aria-label="Page navigation" class="d-flex justify-content-center mt-3">
|
<nav aria-label="Page navigation" class="d-flex justify-content-center mt-3">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
<!-- Previous button -->
|
|
||||||
<li class="page-item @(Model.BlogFilter.PageNumber <= 1 ? "disabled" : "")">
|
<li class="page-item @(Model.BlogFilter.PageNumber <= 1 ? "disabled" : "")">
|
||||||
<a class="page-link" href="javascript:void(0);" onclick="setPageNumber(@(Model.BlogFilter.PageNumber - 1))" aria-label="Previous">
|
<a class="page-link" href="@BlogUrlHelper.GetPageUrl(Model.BlogFilter.PageNumber - 1, Model.BlogFilter)" aria-label="Previous">
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Page numbers -->
|
|
||||||
@for (int i = 1; i <= Model.BlogFilter.TotalPages; i++)
|
@for (int i = 1; i <= Model.BlogFilter.TotalPages; i++)
|
||||||
{
|
{
|
||||||
|
var url = BlogUrlHelper.GetPageUrl(i, Model.BlogFilter);
|
||||||
<li class="page-item @(Model.BlogFilter.PageNumber == i ? "active" : "")">
|
<li class="page-item @(Model.BlogFilter.PageNumber == i ? "active" : "")">
|
||||||
<a class="page-link" href="javascript:void(0);" onclick="setPageNumber(@i)">@i</a>
|
<a class="page-link" href="@url">@i</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Next button -->
|
|
||||||
<li class="page-item @(Model.BlogFilter.PageNumber >= Model.BlogFilter.TotalPages ? "disabled" : "")">
|
<li class="page-item @(Model.BlogFilter.PageNumber >= Model.BlogFilter.TotalPages ? "disabled" : "")">
|
||||||
<a class="page-link" href="javascript:void(0);" onclick="setPageNumber(@(Model.BlogFilter.PageNumber + 1))" aria-label="Next">
|
<a class="page-link" href="@BlogUrlHelper.GetPageUrl(Model.BlogFilter.PageNumber + 1, Model.BlogFilter)" aria-label="Next">
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -127,9 +98,13 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if (Model.IsMobile)
|
@if (Model.IsMobile)
|
||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -140,7 +115,11 @@
|
|||||||
<div class="card-body d-flex align-items-center">
|
<div class="card-body d-flex align-items-center">
|
||||||
<div class="row w-100">
|
<div class="row w-100">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl" class="me-3">
|
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl"
|
||||||
|
asp-route-PageNumber="@Model.BlogFilter.PageNumber"
|
||||||
|
asp-route-SortDirection="@Model.BlogFilter.SortDirection"
|
||||||
|
asp-route-ResultsPerPage="@Model.BlogFilter.ResultsPerPage"
|
||||||
|
class="me-3">
|
||||||
<responsive-image src="@item.ImageUrl" alt="@item.ImageAlt" class="img-fluid" display-width-percentage="30" style="width: 100%; height: auto;"></responsive-image>
|
<responsive-image src="@item.ImageUrl" alt="@item.ImageAlt" class="img-fluid" display-width-percentage="30" style="width: 100%; height: auto;"></responsive-image>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -150,7 +129,11 @@
|
|||||||
<div class="card-text mb-2" style="max-height: 120px; overflow-y: auto;">
|
<div class="card-text mb-2" style="max-height: 120px; overflow-y: auto;">
|
||||||
@Html.Raw(item.IndexText)
|
@Html.Raw(item.IndexText)
|
||||||
</div>
|
</div>
|
||||||
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl" class="text-dark float-end">Read more...</a>
|
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl"
|
||||||
|
asp-route-PageNumber="@Model.BlogFilter.PageNumber"
|
||||||
|
asp-route-SortDirection="@Model.BlogFilter.SortDirection"
|
||||||
|
asp-route-ResultsPerPage="@Model.BlogFilter.ResultsPerPage"
|
||||||
|
class="text-dark float-end">Read more...</a>
|
||||||
<div class="text-muted mt-2">@item.PublishDate.ToString("dd MMMM yyyy")</div>
|
<div class="text-muted mt-2">@item.PublishDate.ToString("dd MMMM yyyy")</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -163,6 +146,25 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@if (!string.IsNullOrEmpty(Model.NextBlog.Title))
|
||||||
|
{
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<div class="card h-100 shadow-lg border-3 border-dark">
|
||||||
|
<div class="row g-0">
|
||||||
|
<div class="col-md-1">
|
||||||
|
<responsive-image src="@Model.NextBlog.ImageUrl" class="img-fluid rounded-start-3" alt="@Model.NextBlog.ImageAlt" display-width-percentage="30"></responsive-image>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-11">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="h5">Coming up in @Model.NextBlog.PublishDate.Humanize(true) - <em>@Model.NextBlog.Title</em></p>
|
||||||
|
<p class="card-text">@Html.Raw(Model.NextBlog.IndexText)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@foreach (var item in Model.Blogs)
|
@foreach (var item in Model.Blogs)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -172,12 +174,21 @@ else
|
|||||||
<h2 class="h5 card-title">@item.Title</h2>
|
<h2 class="h5 card-title">@item.Title</h2>
|
||||||
<small class="text-muted">@item.SubTitle</small>
|
<small class="text-muted">@item.SubTitle</small>
|
||||||
</div>
|
</div>
|
||||||
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl">
|
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl"
|
||||||
|
asp-route-PageNumber="@Model.BlogFilter.PageNumber"
|
||||||
|
asp-route-SortDirection="@Model.BlogFilter.SortDirection"
|
||||||
|
asp-route-ResultsPerPage="@Model.BlogFilter.ResultsPerPage">
|
||||||
<responsive-image src="@item.ImageUrl" alt="@item.ImageAlt" class="card-img rounded-0" display-width-percentage="30"></responsive-image>
|
<responsive-image src="@item.ImageUrl" alt="@item.ImageAlt" class="card-img rounded-0" display-width-percentage="30"></responsive-image>
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">@Html.Raw(item.IndexText)</p>
|
<p class="card-text">@Html.Raw(item.IndexText)</p>
|
||||||
<p class="float-end"><a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl" class="text-primary-emphasis">Read more...</a></p>
|
<p class="float-end">
|
||||||
|
<a asp-controller="TheAlphaFlame" asp-action="BlogItem" asp-route-slug="@item.BlogUrl"
|
||||||
|
asp-route-PageNumber="@Model.BlogFilter.PageNumber"
|
||||||
|
asp-route-SortDirection="@Model.BlogFilter.SortDirection"
|
||||||
|
asp-route-ResultsPerPage="@Model.BlogFilter.ResultsPerPage"
|
||||||
|
class="text-primary-emphasis">Read more...</a>
|
||||||
|
</p>
|
||||||
<h6>@item.PublishDate.ToString("dd MMMM yyyy")</h6>
|
<h6>@item.PublishDate.ToString("dd MMMM yyyy")</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,6 +207,15 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="col-12 mb-3">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="h5">Got an idea for a future blog post?</p>
|
||||||
|
<p>If you would like to ask me a question or have a suggestion for a future blog them please don't be shy. <a asp-controller="AskAQuestion" asp-action="Index"> Go to my ask a question form and let me know.</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@section Meta {
|
@section Meta {
|
||||||
@ -243,18 +263,17 @@ else
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
function setPageNumber(pageNumber) {
|
$(document).on('change', '.js-submit-on-change', function () {
|
||||||
// Set the hidden PageNumber input value
|
|
||||||
document.getElementById('PageNumber').value = pageNumber;
|
|
||||||
|
|
||||||
// Submit the form
|
|
||||||
document.querySelector('form').submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).on('change', '.js-submit-on-change', function(e){
|
|
||||||
$(this).closest('form').submit();
|
$(this).closest('form').submit();
|
||||||
})
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
function disableSubmit(form) {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = 'Sending...'; // Optional
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<h1 class="d-inline-block">@Model.Title</h1><br /><h2 class="h4 d-inline-block">@Model.SubTitle</h2>
|
<h1 class="d-inline-block">@Model.Title</h1><br /><h2 class="h4 d-inline-block">@Model.SubTitle</h2>
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<img src="~/images/webp/@Model.PostedImage" class="rounded-circle border border-dark" style="height: 50px;" />
|
<img src="~/images/webp/@Model.PostedImage" alt="@Model.PostedBy Lynwood" class="rounded-circle border border-dark" style="height: 50px;" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
Posted on @Model.PublishDate.ToString("MMMM d, yyyy") by @Model.PostedBy Lynwood
|
Posted on @Model.PublishDate.ToString("MMMM d, yyyy") by @Model.PostedBy Lynwood
|
||||||
@ -184,6 +184,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function disableSubmit(form) {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = 'Sending...'; // Optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@section Meta {
|
@section Meta {
|
||||||
@ -192,7 +203,7 @@
|
|||||||
meta-description="@Model.IndexText"
|
meta-description="@Model.IndexText"
|
||||||
meta-keywords="Catherine Lynwood blog, The Alpha Flame, psychological thriller, indie fiction, 1980s fiction, dark secrets, strong female characters"
|
meta-keywords="Catherine Lynwood blog, The Alpha Flame, psychological thriller, indie fiction, 1980s fiction, dark secrets, strong female characters"
|
||||||
meta-author="Catherine Lynwood"
|
meta-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/@Model.BlogUrl"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/blog/@Model.BlogUrl"
|
||||||
meta-image="@(string.IsNullOrWhiteSpace(Model.ImageUrl) ? null : $"https://www.catherinelynwood.com/images/webp/{Model.DefaultWebpImage}")"
|
meta-image="@(string.IsNullOrWhiteSpace(Model.ImageUrl) ? null : $"https://www.catherinelynwood.com/images/webp/{Model.DefaultWebpImage}")"
|
||||||
meta-image-alt="@Model.ImageAlt"
|
meta-image-alt="@Model.ImageAlt"
|
||||||
og-site-name="Catherine Lynwood - The Alpha Flame"
|
og-site-name="Catherine Lynwood - The Alpha Flame"
|
||||||
@ -201,7 +212,7 @@
|
|||||||
twitter-card-type="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? "summary_large_image" : "player")"
|
twitter-card-type="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? "summary_large_image" : "player")"
|
||||||
twitter-site-handle="@@CathLynwood"
|
twitter-site-handle="@@CathLynwood"
|
||||||
twitter-creator-handle="@@CathLynwood"
|
twitter-creator-handle="@@CathLynwood"
|
||||||
twitter-video-url="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : Model.VideoUrl)"
|
twitter-video-url="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : $"https://www.catherinelynwood.com/videos/{Model.VideoUrl}")"
|
||||||
twitter-player-width="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 480)"
|
twitter-player-width="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 480)"
|
||||||
twitter-player-height="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 80)" />
|
twitter-player-height="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 80)" />
|
||||||
|
|
||||||
|
|||||||
92
CatherineLynwood/Views/TheAlphaFlame/Enter.cshtml
Normal file
92
CatherineLynwood/Views/TheAlphaFlame/Enter.cshtml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@model CatherineLynwood.Models.Marketing
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Enter The Alpha Flame Discovery Giveaway";
|
||||||
|
}
|
||||||
|
<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="TheAlphaFlame" asp-action="Giveaways">Giveaways</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Enter</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h1 class="h2 mb-4">To enter our giveaway simply complete the entry form below</h1>
|
||||||
|
|
||||||
|
<form asp-action="Enter" id="entryForm" class="row">
|
||||||
|
<div class="col-12 text-dark">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input asp-for="Name" name="Name" class="form-control">
|
||||||
|
<label asp-for="Name"></label>
|
||||||
|
<span asp-validation-for="Name" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-dark">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input asp-for="Email" name="Email" type="email" class="form-control">
|
||||||
|
<label asp-for="Email"></label>
|
||||||
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<p>You don't have to tell me your age or sex, but it would really help me understand my readers if you do. If you don't want to then simply select the "Prefer not to say" option.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-dark">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<select asp-for="Age" name="Age" class="form-select" aria-label="Select your age">
|
||||||
|
<option value="" selected>Select your age</option>
|
||||||
|
<option value="18-24">18–24</option>
|
||||||
|
<option value="25-34">25–34</option>
|
||||||
|
<option value="35-44">35–44</option>
|
||||||
|
<option value="45-54">45–54</option>
|
||||||
|
<option value="55-64">55–64</option>
|
||||||
|
<option value="65+">65+</option>
|
||||||
|
<option value="0">Prefer not to say</option>
|
||||||
|
</select>
|
||||||
|
<label asp-for="Age"></label>
|
||||||
|
<span asp-validation-for="Age" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-dark">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<select asp-for="Sex" name="Sex" class="form-select" aria-label="Select your sex">
|
||||||
|
<option value="" selected>Select Sex</option>
|
||||||
|
<option value="female">Female</option>
|
||||||
|
<option value="male">Male</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
<option value="prefer-not-to-say">Prefer not to say</option>
|
||||||
|
</select>
|
||||||
|
<label asp-for="Sex"></label>
|
||||||
|
<span asp-validation-for="Sex" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="small">
|
||||||
|
By entering this giveaway and joining the newsletter, you consent to receive emails about The Alpha Flame Trilogy, future books, special promotions, and exclusive author updates. You can unsubscribe at any time using the link in any email. View our <a href="/Privacy">privacy policy</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 d-grid">
|
||||||
|
<button type="submit" form="entryForm" class="btn btn-primary">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Meta{
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,13 +2,26 @@
|
|||||||
ViewData["Title"] = "Giveaways - The Alpha Flame";
|
ViewData["Title"] = "Giveaways - The Alpha Flame";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<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">Giveaways</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="py-3 text-center">
|
<section class="py-3 text-center">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card bg-dark text-white">
|
<div class="card bg-dark text-white border-primary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h1 class="display-4 fw-bold">Win a Collector’s Edition</h1>
|
<h1 class="display-4 fw-bold">Win a Collector’s Edition</h1>
|
||||||
<p class="lead mb-4">A special chance to own <em>The Alpha Flame: Discovery</em> in a limited edition, signed or delivered to you.</p>
|
<p class="lead mb-4">A special chance to own <em>The Alpha Flame: Discovery</em> in a limited edition, signed or delivered to you.</p>
|
||||||
<a href="/newsletter/signup" class="btn btn-primary btn-lg">Enter Now</a>
|
<a asp-action="Enter" class="btn btn-primary btn-lg">Enter Now</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -51,20 +64,20 @@
|
|||||||
|
|
||||||
<section class="py-3">
|
<section class="py-3">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card bg-dark text-white">
|
<div class="card bg-dark text-white border-primary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h4 fw-bold text-center mb-4">The Prize</h2>
|
<h2 class="h4 fw-bold text-center mb-4">The Prize</h2>
|
||||||
<div class="row text-center">
|
<div class="row text-center">
|
||||||
<div class="col-md-4 mb-4">
|
<div class="col-md-4 mb-4">
|
||||||
<responsive-image src="the-alpha-flame-11.png" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" alt="Signed Collector’s Edition" display-width-percentage="50"></responsive-image>
|
<responsive-image src="the-alpha-flame-discovery-stood-up.png" class="img-fluid rounded-5 border border-3 border-primary shadow-lg" alt="Signed Collector’s Edition" display-width-percentage="50"></responsive-image>
|
||||||
<p class="mt-2">Signed by the author (UK only)</p>
|
<p class="mt-2">Signed by the author (UK only)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-4">
|
<div class="col-md-4 mb-4">
|
||||||
<responsive-image src="the-alpha-flame-11.png" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" alt="Exclusive cover detail" display-width-percentage="50"></responsive-image>
|
<responsive-image src="the-alpha-flame-discovery-open-on-the-table-maggie.png" class="img-fluid rounded-5 border border-3 border-primary shadow-lg" alt="Exclusive imagery" display-width-percentage="50"></responsive-image>
|
||||||
<p class="mt-2">Exclusive cover design</p>
|
<p class="mt-2">Exclusive imagery</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-4">
|
<div class="col-md-4 mb-4">
|
||||||
<responsive-image src="the-alpha-flame-11.png" class="img-fluid rounded-5 border border-3 border-dark shadow-lg" alt="Interior preview" display-width-percentage="50"></responsive-image>
|
<responsive-image src="the-alpha-flame-discovery-open-on-the-table-beth.png" class="img-fluid rounded-5 border border-3 border-primary shadow-lg" alt="Interior preview" display-width-percentage="50"></responsive-image>
|
||||||
<p class="mt-2">Premium print quality</p>
|
<p class="mt-2">Premium print quality</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -78,7 +91,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="h4 fw-bold mb-3">Don’t Miss Out</h2>
|
<h2 class="h4 fw-bold mb-3">Don’t Miss Out</h2>
|
||||||
<p class="mb-4">Sign up today for your chance to win. Plus, get exclusive updates, early previews, and behind-the-scenes content.</p>
|
<p class="mb-4">Sign up today for your chance to win. Plus, get exclusive updates, early previews, and behind-the-scenes content.</p>
|
||||||
<a href="/newsletter/signup" class="btn btn-dark btn-lg">Enter the Giveaway</a>
|
<a asp-action="Enter" class="btn btn-dark btn-lg">Enter the Giveaway</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -88,7 +101,7 @@
|
|||||||
meta-keywords="The Alpha Flame giveaway, signed book giveaway, Catherine Lynwood, book contest, limited edition book"
|
meta-keywords="The Alpha Flame giveaway, signed book giveaway, Catherine Lynwood, book contest, limited edition book"
|
||||||
meta-author="Catherine Lynwood"
|
meta-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/the-alpha-flame/giveaways"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/giveaways"
|
||||||
meta-image="https://www.catherinelynwood.com/images/alpha-flame-collector-edition.webp"
|
meta-image="https://www.catherinelynwood.com/images/webp/the-alpha-flame-discovery-stood-up-600.webp"
|
||||||
meta-image-alt="The Alpha Flame: Discovery Collector’s Edition Giveaway"
|
meta-image-alt="The Alpha Flame: Discovery Collector’s Edition Giveaway"
|
||||||
og-site-name="Catherine Lynwood – The Alpha Flame"
|
og-site-name="Catherine Lynwood – The Alpha Flame"
|
||||||
article-published-time="@new DateTime(2025, 07, 01)" />
|
article-published-time="@new DateTime(2025, 07, 01)" />
|
||||||
|
|||||||
@ -30,12 +30,12 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-3 d-none d-md-block">
|
<div class="col-md-3 d-none d-md-block">
|
||||||
<a href="/the-alpha-flame/discovery">
|
<a href="/the-alpha-flame/discovery">
|
||||||
<responsive-image src="the-alpha-flame-11.png" class="img-fluid rounded-start-3" alt="The Alpha Flame: Discovery" display-width-percentage="50"></responsive-image>
|
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-start-3" alt="The Alpha Flame: Discovery" display-width-percentage="50"></responsive-image>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 d-md-none">
|
<div class="col-md-3 d-md-none">
|
||||||
<a href="/the-alpha-flame/discovery">
|
<a href="/the-alpha-flame/discovery">
|
||||||
<responsive-image src="the-alpha-flame-11.png" class="img-fluid rounded-top-3" alt="The Alpha Flame: Discovery" display-width-percentage="50"></responsive-image>
|
<responsive-image src="the-alpha-flame-discovery-cover.png" class="img-fluid rounded-top-3" alt="The Alpha Flame: Discovery" display-width-percentage="50"></responsive-image>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
|||||||
@ -172,7 +172,7 @@
|
|||||||
meta-description="@Model.IndexText"
|
meta-description="@Model.IndexText"
|
||||||
meta-keywords="Catherine Lynwood blog, The Alpha Flame, psychological thriller, indie fiction, 1980s fiction, dark secrets, strong female characters"
|
meta-keywords="Catherine Lynwood blog, The Alpha Flame, psychological thriller, indie fiction, 1980s fiction, dark secrets, strong female characters"
|
||||||
meta-author="Catherine Lynwood"
|
meta-author="Catherine Lynwood"
|
||||||
meta-url="https://www.catherinelynwood.com/@Model.BlogUrl"
|
meta-url="https://www.catherinelynwood.com/the-alpha-flame/blog/@Model.BlogUrl"
|
||||||
meta-image="@(string.IsNullOrWhiteSpace(Model.ImageUrl) ? null : $"https://www.catherinelynwood.com/images/webp/{Model.DefaultWebpImage}")"
|
meta-image="@(string.IsNullOrWhiteSpace(Model.ImageUrl) ? null : $"https://www.catherinelynwood.com/images/webp/{Model.DefaultWebpImage}")"
|
||||||
meta-image-alt="@Model.ImageAlt"
|
meta-image-alt="@Model.ImageAlt"
|
||||||
og-site-name="Catherine Lynwood - The Alpha Flame"
|
og-site-name="Catherine Lynwood - The Alpha Flame"
|
||||||
@ -181,7 +181,7 @@
|
|||||||
twitter-card-type="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? "summary_large_image" : "player")"
|
twitter-card-type="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? "summary_large_image" : "player")"
|
||||||
twitter-site-handle="@@CathLynwood"
|
twitter-site-handle="@@CathLynwood"
|
||||||
twitter-creator-handle="@@CathLynwood"
|
twitter-creator-handle="@@CathLynwood"
|
||||||
twitter-video-url="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : Model.VideoUrl)"
|
twitter-video-url="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : $"https://www.catherinelynwood.com/videos/{Model.VideoUrl}")"
|
||||||
twitter-player-width="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 480)"
|
twitter-player-width="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 480)"
|
||||||
twitter-player-height="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 80)" />
|
twitter-player-height="@(string.IsNullOrWhiteSpace(Model.VideoUrl) ? null : 80)" />
|
||||||
|
|
||||||
@ -190,6 +190,17 @@
|
|||||||
@Html.Raw(Model.SchemaJsonLd)
|
@Html.Raw(Model.SchemaJsonLd)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function disableSubmit(form) {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerText = 'Sending...'; // Optional
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
ul {
|
ul {
|
||||||
|
|||||||
50
CatherineLynwood/Views/TheAlphaFlame/Thankyou.cshtml
Normal file
50
CatherineLynwood/Views/TheAlphaFlame/Thankyou.cshtml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
@model CatherineLynwood.Models.Marketing
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Enter The Alpha Flame Discovery Giveaway";
|
||||||
|
}
|
||||||
|
|
||||||
|
<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="TheAlphaFlame" asp-action="Giveaways">Giveaways</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Thank you</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<section class="py-5 bg-dark text-center rounded-3 border border-3 border-light">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-4 fw-bold mb-4">Thank You for Joining the Journey</h1>
|
||||||
|
<p class="lead mb-4">
|
||||||
|
Your entry has been received, and I'm truly grateful you've chosen to be part of The Alpha Flame community.
|
||||||
|
Whether you're here for the giveaway or to follow my writing journey, it means the world to me.
|
||||||
|
</p>
|
||||||
|
<p class="mb-4">
|
||||||
|
As a subscriber, you'll get exclusive updates about <em>The Alpha Flame Trilogy</em>, sneak peeks at future books,
|
||||||
|
behind-the-scenes notes, and other special surprises. I promise to respect your inbox and only send things worth reading.
|
||||||
|
</p>
|
||||||
|
<p class="mb-4">
|
||||||
|
The giveaway closes on <strong>31st August 2025</strong>. The lucky winner will be announced on <strong>7th September 2025</strong>
|
||||||
|
via email and on the Giveaways page. Keep an eye out!
|
||||||
|
</p>
|
||||||
|
<a asp-controller="TheAlphaFlame" asp-action="Index" class="btn btn-primary btn-lg">Explore The Alpha Flame</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Meta {
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<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="Blog">The Alpha Flame Blog</a></li>
|
<li class="breadcrumb-item"><a href="@ViewBag.BlogReturnLink">A Cuppa With Catherine</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">@Model.Title</li>
|
<li class="breadcrumb-item active" aria-current="page">@Model.Title</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -2,13 +2,23 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=localhost;Database=CatherineLynwood;Trusted_Connection=True;MultipleActiveResultSets=true;Encrypt=false;"
|
"DefaultConnection": "Server=localhost;Database=CatherineLynwood;Trusted_Connection=True;MultipleActiveResultSets=true;Encrypt=false;"
|
||||||
},
|
},
|
||||||
"Email": {
|
"Smtp": {
|
||||||
"APIKey": "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"
|
"Host": "smtpout.secureserver.net",
|
||||||
|
"Port": "587",
|
||||||
|
"Sender": "your-email@catherinelynwood.com",
|
||||||
|
"Username": "catherine@catherinelynwood.com",
|
||||||
|
"Password": "ryaN9982?"
|
||||||
},
|
},
|
||||||
"AudioSecurity": {
|
"AudioSecurity": {
|
||||||
"HmacSecretKey": "SG.7xaVKHzRQsS5os1IJUJZ2Q.2osFDJIRkjlDl3eM05uZ9R1IUA6Wv",
|
"HmacSecretKey": "SG.7xaVKHzRQsS5os1IJUJZ2Q.2osFDJIRkjlDl3eM05uZ9R1IUA6Wv",
|
||||||
"TokenExpirySeconds": 86400
|
"TokenExpirySeconds": 86400
|
||||||
},
|
},
|
||||||
|
"ApiKeys": {
|
||||||
|
"BlogPost": "d73dbc3429dh3ycn79f3dfc0nfhyu98q"
|
||||||
|
},
|
||||||
|
"IndexNow": {
|
||||||
|
"ApiKey": "cc6ff72c3d1a48d0b0b7c2c2b543f15f"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@ -1,15 +1,26 @@
|
|||||||
{
|
{
|
||||||
"/the-alpha-flame/chapters/chapter-2-maggie": "/the-alpha-flame/discovery/chapters/chapter-2-maggie",
|
"/5-reasons-to-preorder-the-alpha-flame": "/the-alpha-flame/blog/5-reasons-to-preorder-the-alpha-flame",
|
||||||
"/the-alpha-flame/chapters/chapter-1-beth": "/the-alpha-flame/discovery/chapters/chapter-1-beth",
|
"/asking-for-reviews": "/the-alpha-flame/blog/asking-for-reviews",
|
||||||
|
"/books-like-verity": "/the-alpha-flame/blog/books-like-verity",
|
||||||
|
"/if-you-loved-gone-girl": "/the-alpha-flame/blog/if-you-loved-gone-girl",
|
||||||
"/sisterhood-blog-highlight": "/the-alpha-flame/blog/sisterhood-survival-self-discovery-books",
|
"/sisterhood-blog-highlight": "/the-alpha-flame/blog/sisterhood-survival-self-discovery-books",
|
||||||
"/the-alpha-flame/front-cover": "/the-alpha-flame/discovery",
|
|
||||||
"/the-alpah-flame/blog/1983-birmingham-behind-the-alpha-flame": "/the-alpha-flame/blog/1983-birmingham-behind-the-alpha-flame",
|
"/the-alpah-flame/blog/1983-birmingham-behind-the-alpha-flame": "/the-alpha-flame/blog/1983-birmingham-behind-the-alpha-flame",
|
||||||
|
"/the-alpah-flame/blog/books-like-verity": "/the-alpha-flame/blog/books-like-verity",
|
||||||
"/the-alpah-flame/blog/sisterhood-survival-self-discovery-books": "/the-alpha-flame/blog/sisterhood-survival-self-discovery-books",
|
"/the-alpah-flame/blog/sisterhood-survival-self-discovery-books": "/the-alpha-flame/blog/sisterhood-survival-self-discovery-books",
|
||||||
"/the-alpah-flame/blog/who-is-maggie-grant-alpha-flame": "/the-alpha-flame/blog/who-is-maggie-grant-alpha-flame",
|
"/the-alpah-flame/blog/who-is-maggie-grant-alpha-flame": "/the-alpha-flame/blog/who-is-maggie-grant-alpha-flame",
|
||||||
|
"/the-alpah-flame/blog/why-do-i-bother-indie-author-life": "/the-alpha-flame/blog/why-do-i-bother-indie-author-life",
|
||||||
"/the-alpah-flame/blog/why-i-wrote-the-alpha-flame": "/the-alpha-flame/blog/why-i-wrote-the-alpha-flame",
|
"/the-alpah-flame/blog/why-i-wrote-the-alpha-flame": "/the-alpha-flame/blog/why-i-wrote-the-alpha-flame",
|
||||||
"/the-alpah-flame/blog/books-like-verity": "/the-alpha-flame/blog/books-like-verity",
|
"/the-alpha-flame/chapters/chapter-13-susie": "/the-alpha-flame/discovery/chapters/chapter-13-susie",
|
||||||
|
"/the-alpha-flame/chapters/chapter-1-beth": "/the-alpha-flame/discovery/chapters/chapter-1-beth",
|
||||||
|
"/the-alpha-flame/chapters/chapter-2-maggie": "/the-alpha-flame/discovery/chapters/chapter-2-maggie",
|
||||||
|
"/the-alpha-flame/characters/beth": "/the-alpha-flame/characters/beth-fletcher",
|
||||||
"/TheAlphaFlame/Discovery": "/the-alpha-flame/discovery",
|
"/TheAlphaFlame/Discovery": "/the-alpha-flame/discovery",
|
||||||
"/the-alpha-flame/meet-the-characters": "/the-alpha-flame/characters",
|
"/the-alpha-flame/front-cover": "/the-alpha-flame/discovery",
|
||||||
"/the-alpha-flame/maggie-grant": "/the-alpha-flame/characters/maggie-grant",
|
"/the-alpha-flame/maggie-grant": "/the-alpha-flame/characters/maggie-grant",
|
||||||
"/the-alpha-flame/characters/beth": "/the-alpha-flame/characters/beth-fletcher"
|
"/the-alpha-flame/meet-the-characters": "/the-alpha-flame/characters",
|
||||||
|
"/the-broken-girl-in-fiction": "/the-alpha-flame/blog/the-broken-girl-in-fiction",
|
||||||
|
"/up-and-coming-indie-author": "/the-alpha-flame/blog/up-and-coming-indie-author",
|
||||||
|
"/who-is-maggie-grant-alpha-flame": "/the-alpha-flame/blog/who-is-maggie-grant-alpha-flame",
|
||||||
|
"/why-do-i-bother-indie-author-life": "/the-alpha-flame/blog/why-do-i-bother-indie-author-life",
|
||||||
|
"/why-verity-fans-will-love-the-alpha-flame": "/the-alpha-flame/blog/why-verity-fans-will-love-the-alpha-flame"
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
cc6ff72c3d1a48d0b0b7c2c2b543f15f
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user