- Refactored `BuyController` for improved click logging and centralized link management. - Updated `DiscoveryController` to enhance user request handling and added country context dependency. - Introduced `ClicksController` for managing click tracking and redirects. - Added `GeoResolutionMiddleware` to resolve user locations based on IP. - Created `BuyCatalog` for centralized management of buy links. - Introduced view models (`BuyLinksViewModel`, `DiscoveryPageViewModel`) for better data handling in views. - Added new files for geographical data handling (`GeoIpResult`, `HttpContextItemKeys`, `LinkChoice`, etc.). - Updated `_BuyBox.cshtml` to render buy options based on user location. - Modified `DataAccess` for saving and retrieving geographical data.
367 lines
16 KiB
Plaintext
367 lines
16 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="en-GB">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>@ViewData["Title"] | Catherine Lynwood</title>
|
||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" asp-append-version="true" />
|
||
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
|
||
<link rel="stylesheet" href="~/css/fontawesome.min.css" asp-append-version="true" />
|
||
<link rel="stylesheet" href="~/css/brands.min.css" asp-append-version="true" />
|
||
<link rel="stylesheet" href="~/css/duotone.min.css" asp-append-version="true" />
|
||
<link rel="stylesheet" href="~/css/plyr.min.css" asp-append-version="true" />
|
||
|
||
<script>
|
||
// Load Meta Pixel ONLY if consent was given
|
||
(function () {
|
||
if (localStorage.getItem('marketingConsent') !== 'yes') return;
|
||
|
||
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
||
n.push=n; n.loaded=!0; n.version='2.0'; n.queue=[]; t=b.createElement(e); t.async=!0;
|
||
t.src=v; s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||
'https://connect.facebook.net/en_US/fbevents.js');
|
||
|
||
fbq('init', '556474687460834');
|
||
fbq('track', 'PageView', {
|
||
page_location: location.href,
|
||
page_path: location.pathname,
|
||
page_title: document.title,
|
||
referrer: document.referrer || null
|
||
});
|
||
})();
|
||
</script>
|
||
<noscript>
|
||
<!-- harmless if present; loads only when JS disabled -->
|
||
<img height="1" width="1" style="display:none"
|
||
src="https://www.facebook.com/tr?id=YOUR_PIXEL_ID&ev=PageView&noscript=1"/>
|
||
</noscript>
|
||
|
||
<!-- End Meta Pixel -->
|
||
|
||
|
||
<style>
|
||
.plyr--audio .plyr__controls {
|
||
background-color: rgba(255,255,255,1) !important; /* or set your page colour */
|
||
box-shadow: none !important; /* optional: remove shadow */
|
||
border-radius: 15px; /* optional: adjust corners */
|
||
border-color: black;
|
||
border-width: 2px;
|
||
border-style: solid;
|
||
}
|
||
|
||
.plyr--audio {
|
||
background-color: transparent !important; /* background of the whole player */
|
||
}
|
||
|
||
</style>
|
||
|
||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
|
||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
|
||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
|
||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
|
||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
|
||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
|
||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
|
||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
|
||
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||
<link rel="manifest" href="/manifest.json">
|
||
<meta name="msapplication-TileColor" content="#17C8EB">
|
||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
||
<meta name="theme-color" content="#17C8EB">
|
||
|
||
|
||
|
||
<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}")" />
|
||
|
||
@RenderSection("Meta", required: false)
|
||
|
||
</head>
|
||
<body class="bg-primary text-white">
|
||
<div id="background-wrapper">
|
||
<div class="video-background">
|
||
<video id="siteBackgroundVideo"
|
||
autoplay
|
||
muted
|
||
loop
|
||
playsinline
|
||
preload="none"
|
||
poster="/images/webp/the-alpha-flame-discovery-blank-400.webp">
|
||
<!-- Source will be injected by JS -->
|
||
</video>
|
||
<div class="video-overlay"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="content">
|
||
<!-- Content wrapper to keep everything on top of the background -->
|
||
<header>
|
||
<nav class="navbar fixed-top navbar-expand-sm navbar-toggleable-sm navbar-dark border-bottom border-2 border-primary box-shadow mb-3 bg-dark">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand pe-0 me-0" asp-area="" asp-controller="Home" asp-action="Index">
|
||
<img src="~/images/catherine-lynwood-banner-logo-small.png" alt="Catherine Lynwood Logo Banner" class="img-fluid" style="height: 45px; width: 265px;" />
|
||
</a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
|
||
aria-expanded="false" aria-label="Toggle navigation">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||
<ul class="navbar-nav flex-grow-1">
|
||
<li class="nav-item">
|
||
<a class="nav-link text-primary" asp-area="" asp-controller="Home" asp-action="AboutCatherineLynwood">About Catherine</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link text-primary" asp-area="" asp-controller="TheAlphaFlame" asp-action="Blog">A Cuppa With Catherine</a>
|
||
</li>
|
||
<li class="nav-item dropdown">
|
||
<a class="nav-link dropdown-toggle text-primary" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||
The Alpha Flame
|
||
</a>
|
||
<ul class="dropdown-menu">
|
||
<li class="py-2">
|
||
<a class="dropdown-item" asp-controller="Discovery" asp-action="Index">Discovery (Book 1)</a>
|
||
</li>
|
||
<li class="py-2">
|
||
<a class="dropdown-item" asp-controller="Discovery" asp-action="Trailer">Discovery Release Trailer</a>
|
||
</li>
|
||
<li class="py-2">
|
||
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Index">The Trilogy</a>
|
||
</li>
|
||
<li class="py-2">
|
||
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="StoryMap">Story Map</a>
|
||
</li>
|
||
<li class="py-2">
|
||
<a class="dropdown-item" asp-controller="TheAlphaFlame" asp-action="Characters">Meet the Characters</a>
|
||
</li>
|
||
</ul>
|
||
</li>
|
||
<li class="nav-item">
|
||
<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>
|
||
</ul>
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</nav>
|
||
</header>
|
||
<div class="container-lg" style="margin-top: 90px;">
|
||
<main role="main" class="pb-3">
|
||
@RenderBody()
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Scroll hint -->
|
||
<div class="scroll-hint" id="scrollHint">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</div>
|
||
</div> <!-- End of content wrapper -->
|
||
<footer class="border-top border-2 border-primary footer bg-dark py-3" id="site-footer">
|
||
<div class="container">
|
||
<div class="row justify-content-center align-items-center" >
|
||
<div class="col-md-6 text-center order-md-2">
|
||
<social-media-share title="Catherine Lynwood Blog" url="@Context.Request.Path" class="text-center m-3"></social-media-share>
|
||
© 2024 - @DateTime.Now.Year - Catherine Lynwood
|
||
</div>
|
||
<div class="col-md-3 text-center order-md-1">
|
||
<p>
|
||
<a class="text-light" asp-area="" asp-controller="Home" asp-action="ContactCatherine">Contact Catherine</a>
|
||
</p>
|
||
<p>
|
||
<a class="text-light" asp-area="" asp-controller="Publishing" asp-action="Index">Catherine Lynwood Publishing</a>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="col-md-3 text-center order-md-3">
|
||
<p>
|
||
<a class="text-light" asp-area="" asp-controller="Home" asp-action="AboutCatherineLynwood">About Catherine Lynwood</a>
|
||
</p>
|
||
<p>
|
||
<a class="text-light" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy Policy</a>
|
||
</p>
|
||
<p>
|
||
<a class="text-light" href="#" onclick="manageCookies();return false;">Manage cookies</a>
|
||
|
||
</p>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
|
||
<div class="modal fade"
|
||
id="cookieModal"
|
||
tabindex="-1"
|
||
aria-labelledby="cookieTitle"
|
||
aria-hidden="true"
|
||
data-bs-backdrop="static"
|
||
data-bs-keyboard="false"
|
||
data-nosnippet>
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border border-3 border-dark rounded-4 shadow-lg" data-nosnippet>
|
||
<div class="modal-header">
|
||
<h5 class="modal-title text-dark" id="cookieTitle">Cookies & privacy</h5>
|
||
</div>
|
||
<div class="modal-body" data-nosnippet>
|
||
<p class="text-dark mb-2">We use necessary cookies and, with your permission, marketing cookies…</p>
|
||
<p class="small text-muted mb-0">You can change your choice anytime…</p>
|
||
</div>
|
||
<div class="modal-footer d-flex gap-2" data-nosnippet>
|
||
<button type="button" class="btn btn-outline-dark" id="cookieReject">Reject</button>
|
||
<button type="button" class="btn btn-dark" id="cookieAccept">Accept</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||
<script src="~/js/plyr.js"></script>
|
||
|
||
|
||
@* <script>
|
||
const player = new Plyr('audio');
|
||
</script> *@
|
||
@RenderSection("Scripts", required: false)
|
||
|
||
|
||
<script>
|
||
window.addEventListener("load", () => {
|
||
setTimeout(() => {
|
||
const video = document.getElementById("siteBackgroundVideo");
|
||
const source = document.createElement("source");
|
||
source.src = "/videos/background-3.mp4";
|
||
source.type = "video/mp4";
|
||
video.appendChild(source);
|
||
video.load(); // Initiates download
|
||
video.play().catch(err => {
|
||
console.warn("Background video autoplay failed:", err);
|
||
});
|
||
}, 1500); // Adjust delay as needed (e.g. 1000–3000ms)
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
if ('serviceWorker' in navigator) {
|
||
window.addEventListener('load', function () {
|
||
navigator.serviceWorker.register('/service-worker.js')
|
||
.then(function (registration) {
|
||
console.log('ServiceWorker registered with scope:', registration.scope);
|
||
}, function (err) {
|
||
console.log('ServiceWorker registration failed:', err);
|
||
});
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<script type="text/javascript">
|
||
window._mfq = window._mfq || [];
|
||
(function() {
|
||
var mf = document.createElement("script");
|
||
mf.type = "text/javascript"; mf.defer = true;
|
||
mf.src = "//cdn.mouseflow.com/projects/34f4429c-9f27-4c01-9d08-2ac8d7144273.js";
|
||
document.getElementsByTagName("head")[0].appendChild(mf);
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
var hint = document.getElementById('scrollHint');
|
||
var footer = document.getElementById('site-footer');
|
||
if (!hint || !footer) return;
|
||
|
||
var scrollTimer = null;
|
||
|
||
function docHeight() {
|
||
return Math.max(
|
||
document.body.scrollHeight, document.documentElement.scrollHeight,
|
||
document.body.offsetHeight, document.documentElement.offsetHeight,
|
||
document.body.clientHeight, document.documentElement.clientHeight
|
||
);
|
||
}
|
||
function canScroll() {
|
||
return docHeight() > window.innerHeight + 8;
|
||
}
|
||
function atBottom() {
|
||
return (window.scrollY + window.innerHeight) >= (docHeight() - 4);
|
||
}
|
||
function footerVisible() {
|
||
var rect = footer.getBoundingClientRect();
|
||
// footer considered visible if its top edge has entered the viewport
|
||
return rect.top < window.innerHeight;
|
||
}
|
||
|
||
function evaluateHint() {
|
||
// Show only if: page can scroll, not at bottom, and footer NOT yet visible
|
||
if (canScroll() && !atBottom() && !footerVisible()) {
|
||
hint.classList.add('show');
|
||
} else {
|
||
hint.classList.remove('show');
|
||
}
|
||
}
|
||
|
||
// Initial state
|
||
evaluateHint();
|
||
|
||
// Hide while actively scrolling; show again after a short pause
|
||
function onScroll() {
|
||
hint.classList.remove('show');
|
||
if (scrollTimer) clearTimeout(scrollTimer);
|
||
scrollTimer = setTimeout(evaluateHint, 300);
|
||
}
|
||
|
||
window.addEventListener('scroll', onScroll, { passive: true });
|
||
window.addEventListener('resize', function () { setTimeout(evaluateHint, 120); });
|
||
|
||
hint.addEventListener('click', function () {
|
||
window.scrollBy({ top: Math.round(window.innerHeight * 0.8), behavior: 'smooth' });
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
if (!("ping" in HTMLAnchorElement.prototype) && navigator.sendBeacon) {
|
||
document.body.addEventListener("click", e => {
|
||
const a = e.target.closest("a[ping]");
|
||
if (!a) return;
|
||
const url = a.getAttribute("ping");
|
||
if (url) {
|
||
try {
|
||
navigator.sendBeacon(url);
|
||
} catch { /* ignore */ }
|
||
}
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
|
||
|
||
</body>
|
||
|
||
</html>
|