Moved old project components to this new project

This commit is contained in:
2026-05-09 16:46:01 +02:00
parent 1c869b9a7b
commit b08e891020
40 changed files with 2910 additions and 1 deletions
+147
View File
@@ -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">
&copy; 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();
}
}
+127
View File
@@ -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();
}
}