Moved old project components to this new project
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inject IJSRuntime JSRuntime
|
||||
@using Blazored.Toast
|
||||
|
||||
<header class="top-bar">
|
||||
<a href="/" class="brand">
|
||||
<svg class="brand-mark" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70,25 C65,18 58,15 50,15 C30,15 15,30 15,50 C15,70 30,85 50,85 C58,85 65,82 70,75 L62,68 C58,72 54,74 50,74 C37,74 27,63 27,50 C27,37 37,26 50,26 C54,26 58,28 62,32 L70,25 Z" fill="#0096c7" />
|
||||
<circle cx="85" cy="50" r="8" fill="#4dabff" opacity="0.9" />
|
||||
<circle cx="75" cy="80" r="5" fill="#4dabff" opacity="0.6" />
|
||||
<circle cx="75" cy="20" r="5" fill="#4dabff" opacity="0.6" />
|
||||
</svg>
|
||||
|
||||
<div class="text-column">
|
||||
<span class="brand-main">Lite<span class="brand-accent">Charms</span></span>
|
||||
<span class="payoff-line">Affordable Technology Today</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button class="hamburger" @onclick="ToggleMenu" aria-label="Toggle Menu" aria-expanded="@isMenuOpen.ToString().ToLower()">
|
||||
<div class="bar @(isMenuOpen ? "animate" : "")"></div>
|
||||
</button>
|
||||
|
||||
<nav class="nav-links @(isMenuOpen ? "open" : "")">
|
||||
<a href="services" class="nav-link" @onclick="CloseMenu">Services</a>
|
||||
<a href="shop" class="nav-link nav-shop" @onclick="CloseMenu">Shop</a>
|
||||
<a href="about" class="nav-link" @onclick="CloseMenu">About</a>
|
||||
<a href="contact" class="nav-link" @onclick="CloseMenu">Contact</a>
|
||||
<button class="btn-login mobile-only" @onclick="CloseMenu">Login</button>
|
||||
</nav>
|
||||
|
||||
<div class="header-actions desktop-only">
|
||||
<div class="contact-info">
|
||||
<span class="contact-item">
|
||||
<svg class="contact-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
|
||||
+27872650198
|
||||
</span>
|
||||
<span class="blue-dot"></span>
|
||||
@* Refactored: Mailto link with subject and body *@
|
||||
<a href="mailto:contact@litecharms.co.za?subject=Enquiry%20from%20Shop&body=Hi%20Lite%20Charms%20Team," class="contact-item">
|
||||
<svg class="contact-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||
contact@litecharms.co.za
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button class="btn-login">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
|
||||
@* Cookie Banner Added Here *@
|
||||
@if (showBanner)
|
||||
{
|
||||
<div class="cookie-banner">
|
||||
<div class="cookie-content">
|
||||
<p>We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.</p>
|
||||
<div class="cookie-actions">
|
||||
<button class="btn-cookie accept" @onclick="() => HandleConsent(true)">Accept</button>
|
||||
<button class="btn-cookie decline" @onclick="() => HandleConsent(false)">Decline</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<BlazoredToasts />
|
||||
|
||||
<footer class="bottom-bar">
|
||||
<div class="footer-left">
|
||||
© 2026 Lite Charms (PTY) LTD. All rights reserved.
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<a href="terms" class="footer-link">Terms & Conditions</a>
|
||||
<span class="divider">|</span>
|
||||
<a href="privacy" class="footer-link">Privacy Policy</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool isMenuOpen = false;
|
||||
private bool showBanner = false;
|
||||
private bool isInitialized = false;
|
||||
|
||||
private void ToggleMenu()
|
||||
{
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
|
||||
private void CloseMenu()
|
||||
{
|
||||
isMenuOpen = false;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// Only run this logic once when the component first renders
|
||||
if (firstRender && !isInitialized)
|
||||
{
|
||||
isInitialized = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Attempt to read from localStorage
|
||||
var consent = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "litecharms_cookie_consent");
|
||||
|
||||
// If no consent is found, show the banner
|
||||
if (string.IsNullOrWhiteSpace(consent))
|
||||
{
|
||||
await Task.Delay(500); // Wait for page to settle
|
||||
showBanner = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If JS Interop fails (e.g., storage blocked), default to showing the banner
|
||||
Console.WriteLine($"Cookie check failed: {ex.Message}");
|
||||
showBanner = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleConsent(bool accepted)
|
||||
{
|
||||
showBanner = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Save preference
|
||||
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "litecharms_cookie_consent", accepted ? "accepted" : "rejected");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail silently if storage is unavailable
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/* High-visibility Shop link */
|
||||
.nav-links .nav-link.nav-shop {
|
||||
color: var(--brand-blue, #0096c7);
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-links .nav-link.nav-shop:hover {
|
||||
background-color: rgba(0, 150, 199, 0.1);
|
||||
border-color: var(--brand-blue, #0096c7);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Mobile adjustments for the shop button */
|
||||
@media (max-width: 768px) {
|
||||
.nav-links .nav-link.nav-shop {
|
||||
border: 1px solid var(--brand-blue, #0096c7);
|
||||
margin: 5px 0;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 150, 199, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Blazored Toast Branded Styling --- */
|
||||
|
||||
/* 1. The Container: Positioned bottom-left */
|
||||
::deep .blazored-toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px; /* Moved from top to bottom */
|
||||
left: 20px; /* Moved from right to left */
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column-reverse; /* Newest toasts appear on top of old ones */
|
||||
gap: 12px;
|
||||
width: 350px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 2. The Individual Toast */
|
||||
::deep .blazored-toast {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: rgba(11, 17, 20, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(0, 150, 199, 0.3);
|
||||
border-left: 4px solid #0096c7;
|
||||
border-radius: 8px;
|
||||
padding: 16px 40px 16px 16px;
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
animation: slideInLeft 0.3s ease-out; /* Updated animation name */
|
||||
}
|
||||
|
||||
/* 3. Heading & Message */
|
||||
::deep .blazored-toast-header {
|
||||
font-weight: 800;
|
||||
font-size: 1rem;
|
||||
color: #0096c7;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
::deep .blazored-toast-message {
|
||||
font-size: 0.9rem;
|
||||
color: #e0e0e0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 4. The Close (X) Button */
|
||||
::deep .blazored-toast-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
::deep .blazored-toast-close::before {
|
||||
content: "×";
|
||||
}
|
||||
|
||||
::deep .blazored-toast-close:hover {
|
||||
color: #ff4d4d;
|
||||
}
|
||||
|
||||
/* 5. Progress Bar */
|
||||
::deep .blazored-toast-progressbar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: rgba(0, 150, 199, 0.5);
|
||||
}
|
||||
|
||||
/* Entrance Animation: Sliding in from the left */
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Adjustments */
|
||||
@media (max-width: 480px) {
|
||||
::deep .blazored-toast-container {
|
||||
width: calc(100% - 40px);
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
top: auto; /* Ensure it stays at the bottom */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
|
||||
Resume
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user