Stable run with tailwind
This commit is contained in:
@@ -1,53 +1,438 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inherits LayoutComponentBase
|
||||
@namespace MidrandBooks.Components.Layout
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using MidrandBooks.Components
|
||||
@using MidrandBooks.Components.Pages
|
||||
@using MidrandBooks.Models
|
||||
|
||||
<div class="water-background">
|
||||
<div class="relative min-h-screen flex flex-col justify-between selection:bg-violet-500/10 selection:text-violet-700">
|
||||
|
||||
<!-- Dynamic Ambient Motion Background -->
|
||||
<AnimatedBackground />
|
||||
|
||||
<div class="blotch blotch-light-main"></div>
|
||||
<div class="blotch blotch-dark-main"></div>
|
||||
<div class="blotch blotch-accent-1"></div>
|
||||
<div class="blotch blotch-accent-2"></div>
|
||||
<!-- Top Navigation Header -->
|
||||
<Navbar
|
||||
ActiveTab="@ActiveTab"
|
||||
OnActiveTabChanged="HandleActiveTabChanged"
|
||||
OnOpenCart="HandleOpenCart"
|
||||
OnSelectBook="HandleSelectBook"
|
||||
Books="CatalogBooks"
|
||||
CartCount="CartItems.Count"
|
||||
IsOfflineMode="IsOfflineMode"
|
||||
OnOfflineModeToggle="HandleOfflineToggle"
|
||||
/>
|
||||
|
||||
<div class="noise-overlay"></div>
|
||||
<!-- Main Central Work Space -->
|
||||
<main class="flex-1 w-full max-w-7xl mx-auto px-4 sm:px-6 relative z-10 py-6">
|
||||
@if (ActiveTab == "catalog")
|
||||
{
|
||||
<!-- Featured Slide Carousel Section -->
|
||||
<FeaturedCarousel
|
||||
FeaturedBooks="FeaturedBooks"
|
||||
Purchases="PurchasedRecords"
|
||||
Cart="CartItems"
|
||||
OnSelectBook="HandleSelectBook"
|
||||
OnReadEbook="HandleReadEbook"
|
||||
OnStreamAudiobook="HandlePlayAudiobook"
|
||||
OnAddToCart="HandleAddToCart"
|
||||
/>
|
||||
|
||||
<div class="glass-panel">
|
||||
<!-- Main Interactive Catalog Grid -->
|
||||
<BookGrid
|
||||
Books="CatalogBooks"
|
||||
Categories="Categories"
|
||||
OnSelectBook="HandleSelectBook"
|
||||
/>
|
||||
}
|
||||
else if (ActiveTab == "library")
|
||||
{
|
||||
<!-- Library View component -->
|
||||
<LibraryView
|
||||
Purchases="PurchasedRecords"
|
||||
Books="CatalogBooks"
|
||||
OfflineBooks="OfflineCachedBooks"
|
||||
IsOfflineMode="IsOfflineMode"
|
||||
OnSelectBook="HandleSelectBook"
|
||||
OnReadBook="HandleReadEbook"
|
||||
OnPlayBook="HandlePlayAudiobook"
|
||||
OnCacheBookOffline="HandleCacheBookOffline"
|
||||
/>
|
||||
}
|
||||
else if (ActiveTab == "author")
|
||||
{
|
||||
<!-- Author Portal component -->
|
||||
<AuthorPortal
|
||||
Categories="Categories"
|
||||
OnAddCategory="HandleAddCategory"
|
||||
OnPublishBook="HandlePublishBook"
|
||||
/>
|
||||
}
|
||||
else if (ActiveTab == "terms")
|
||||
{
|
||||
<!-- Terms & Conditions view -->
|
||||
<Terms />
|
||||
}
|
||||
else if (ActiveTab == "privacy")
|
||||
{
|
||||
<!-- Privacy Policy view -->
|
||||
<Privacy />
|
||||
}
|
||||
|
||||
<header class="top-bar">
|
||||
<span class="brand-text">midrandbooks.co.za</span>
|
||||
<!-- Fallback or standard Blazor body placeholder -->
|
||||
@Body
|
||||
</main>
|
||||
|
||||
<nav class="top-nav-icons">
|
||||
<button class="icon-btn" title="Ebooks" aria-label="Ebooks">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Detailed Book Information Modal Popup -->
|
||||
@if (SelectedBook != null)
|
||||
{
|
||||
var isEbookOwned = PurchasedRecords.Any(p => p.BookId == SelectedBook.Id && p.Format == "ebook");
|
||||
var isEbookCached = OfflineCachedBooks.Contains(SelectedBook.Id);
|
||||
var isAudioOwned = PurchasedRecords.Any(p => p.BookId == SelectedBook.Id && p.Format == "audiobook");
|
||||
var ebookInCart = CartItems.Any(i => i.Book.Id == SelectedBook.Id && i.Format == "ebook");
|
||||
var audioInCart = CartItems.Any(i => i.Book.Id == SelectedBook.Id && i.Format == "audiobook");
|
||||
|
||||
<button class="icon-btn" title="Audiobooks" aria-label="Audiobooks">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6" />
|
||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z" />
|
||||
</svg>
|
||||
</button>
|
||||
<BookDetailModal
|
||||
Book="SelectedBook"
|
||||
IsEbookOwned="isEbookOwned"
|
||||
IsEbookCached="isEbookCached"
|
||||
IsAudioOwned="isAudioOwned"
|
||||
EbookInCart="ebookInCart"
|
||||
AudioInCart="audioInCart"
|
||||
UserRating="UserRatings.ContainsKey(SelectedBook.Id) ? UserRatings[SelectedBook.Id] : (int?)null"
|
||||
OnClose="HandleCloseModal"
|
||||
OnAddToCart="HandleAddToCartFromModal"
|
||||
OnReadEbook="() => HandleReadEbook(SelectedBook)"
|
||||
OnPlayAudiobook="() => HandlePlayAudiobook(SelectedBook)"
|
||||
OnRateBook="rating => HandleRateBook(SelectedBook.Id, rating)"
|
||||
/>
|
||||
}
|
||||
|
||||
<button class="icon-btn" title="Library" aria-label="Library">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Secure Checkout Side Panel Drawer -->
|
||||
<CartAndCheckout
|
||||
IsOpen="IsCartOpen"
|
||||
OnClose="HandleCloseCart"
|
||||
CartItems="CartItems"
|
||||
OnRemoveFromCart="HandleRemoveFromCart"
|
||||
OnCheckoutSuccess="HandleCheckoutSuccess"
|
||||
/>
|
||||
|
||||
<button class="icon-btn" title="My Collection" aria-label="My Collection">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- Embedded Proprietary Ebook Reader Overlay -->
|
||||
@if (ActiveReadingBook != null)
|
||||
{
|
||||
<EbookReader
|
||||
Book="ActiveReadingBook"
|
||||
CurrentPage="ActiveReadingPage"
|
||||
OnClose="HandleCloseReader"
|
||||
OnPageChanged="HandlePageProgressChanged"
|
||||
/>
|
||||
}
|
||||
|
||||
<main class="content-area">
|
||||
@Body
|
||||
</main>
|
||||
<!-- Persistent Bottom Audiobook Player Node -->
|
||||
@if (ActivePlayingBook != null)
|
||||
{
|
||||
<AudiobookPlayer
|
||||
Book="ActivePlayingBook"
|
||||
IsPlaying="IsAudioPlaying"
|
||||
OnPlayToggle="HandleAudioPlayToggle"
|
||||
OnClose="HandleCloseAudioPlayer"
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Global High Contrast Footer -->
|
||||
<footer class="border-t border-white/5 bg-slate-950 mt-16 py-12 relative z-10">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 flex flex-col md:flex-row items-center justify-between gap-6 text-center md:text-left">
|
||||
<div>
|
||||
<span class="font-sans text-sm font-extrabold text-white tracking-tight uppercase">
|
||||
Midrand<span class="font-light text-slate-300">books</span><span class="text-[10px] font-normal lowercase text-violet-400">.co.za</span>
|
||||
</span>
|
||||
<p class="text-[11px] text-slate-300 mt-1.5 font-sans">
|
||||
Legal Deposit Compliant • National Library Services of South Africa • Est. 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-6 text-[11px] font-mono font-bold uppercase text-slate-300 tracking-wider">
|
||||
<button @onclick='() => HandleActiveTabChanged("catalog")' class="hover:text-white transition-colors cursor-pointer focus:outline-none">Catalog</button>
|
||||
<button @onclick='() => HandleActiveTabChanged("terms")' class="hover:text-white transition-colors cursor-pointer focus:outline-none text-violet-400">Terms of Sale</button>
|
||||
<button @onclick='() => HandleActiveTabChanged("privacy")' class="hover:text-white transition-colors cursor-pointer focus:outline-none text-violet-400">Privacy Policy</button>
|
||||
<a href="#contact" class="hover:text-white transition-colors">Midrand, Gauteng</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center md:items-end gap-1">
|
||||
<p class="text-[10px] text-slate-400 font-mono">
|
||||
© 2026 Midrand Books. Securely compiled under sandbox rules.
|
||||
</p>
|
||||
<p class="text-[10px] text-slate-500 font-mono">
|
||||
Designed, maintained, owned & hosted by <a href="https://www.litecharms.co.za" target="_blank" rel="noopener noreferrer" class="text-violet-400 hover:text-violet-300 underline">LiteCharms (PTY) Ltd</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Signal Interruption Reconnect Modal Overlay -->
|
||||
<ReconnectModal />
|
||||
|
||||
<!-- Cookie Acceptance Banner and Privacy Modal -->
|
||||
<CookieBanner />
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string ActiveTab { get; set; } = "catalog";
|
||||
private bool IsCartOpen { get; set; } = false;
|
||||
private bool IsOfflineMode { get; set; } = false;
|
||||
|
||||
// Master list of categories
|
||||
private List<string> Categories { get; set; } = new()
|
||||
{
|
||||
"Fiction", "Non-Fiction", "African Literature", "Poetry", "Academic & History", "Biography"
|
||||
};
|
||||
|
||||
// Books and States
|
||||
private List<BookModel> CatalogBooks { get; set; } = new();
|
||||
private List<BookModel> FeaturedBooks { get; set; } = new();
|
||||
private BookModel? SelectedBook { get; set; }
|
||||
private List<CartItem> CartItems { get; set; } = new();
|
||||
private List<PurchaseRecord> PurchasedRecords { get; set; } = new();
|
||||
private List<string> OfflineCachedBooks { get; set; } = new();
|
||||
private Dictionary<string, int> UserRatings { get; set; } = new();
|
||||
|
||||
// Active Reader/Player Overlays
|
||||
private BookModel? ActiveReadingBook { get; set; }
|
||||
private int ActiveReadingPage { get; set; } = 0;
|
||||
|
||||
private BookModel? ActivePlayingBook { get; set; }
|
||||
private bool IsAudioPlaying { get; set; } = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Load default premium seed publications
|
||||
var random = new Random(2026);
|
||||
var categoriesList = new[] { "Fiction", "African Literature", "Poetry", "Academic & History", "Biography", "Non-Fiction" };
|
||||
var colors = new[] {
|
||||
"from-indigo-900 to-purple-950",
|
||||
"from-rose-950 to-orange-900",
|
||||
"from-slate-900 to-teal-950",
|
||||
"from-amber-950 to-emerald-950"
|
||||
};
|
||||
|
||||
var titles = new[] {
|
||||
"Whispers of the Highveld", "Gauteng Gold Rush Chronicles", "Soweto Symphony of Dreams",
|
||||
"Limpopo Rain Queen", "The Karoo Echoes", "Table Mountain Sunrise",
|
||||
"Durban Beachfront Rhythms", "Zululand Warriors"
|
||||
};
|
||||
|
||||
var authors = new[] {
|
||||
"N. Khumalo", "S. Ndlovu", "T. Mokoena", "K. Naidoo", "M. van der Merwe",
|
||||
"L. Sibanda", "J. Govender", "Z. Zuma"
|
||||
};
|
||||
|
||||
var descriptions = new[] {
|
||||
"An epic South African saga highlighting historical transformations, personal sacrifices, and cultural richness in the Gauteng plains.",
|
||||
"A comprehensive investigative review documenting the rich socio-economic changes driven by gold discoveries in early Johannesburg.",
|
||||
"A beautifully written modern poetry collection exploring life, struggle, and celebratory musical triumphs in South Africa's heartland.",
|
||||
"An enchanting fantasy drawing on classical folklore, rainmaking traditions, and political intrigue in the northern provinces.",
|
||||
"A mystery novel detailing hidden family histories buried deep in the silent, expansive landscapes of the great Karoo desert."
|
||||
};
|
||||
|
||||
for (int i = 0; i < titles.Length; i++)
|
||||
{
|
||||
var book = new BookModel
|
||||
{
|
||||
Id = $"book-seed-{i + 1}",
|
||||
Title = titles[i],
|
||||
Author = authors[i],
|
||||
AuthorId = $"seed-auth-{i + 1}",
|
||||
Description = descriptions[i % descriptions.Length],
|
||||
Price = random.Next(95, 295),
|
||||
Category = categoriesList[i % categoriesList.Length],
|
||||
CoverColor = colors[i % colors.Length],
|
||||
Rating = 4.0 + random.NextDouble(),
|
||||
ReviewsCount = random.Next(5, 85),
|
||||
PublishedYear = random.Next(2018, 2026),
|
||||
PagesCount = random.Next(120, 480),
|
||||
AudioDuration = $"{random.Next(4, 12)}h {random.Next(10, 59)}m",
|
||||
Narrator = authors[(i + 2) % authors.Length],
|
||||
Bestseller = i < 2,
|
||||
Trending = i >= 2 && i < 4,
|
||||
Status = "published"
|
||||
};
|
||||
|
||||
// Populate some dummy chapters
|
||||
book.FullText = new List<string>
|
||||
{
|
||||
$"Chapter 1 of {book.Title}.\n\nThe morning sun breaks across the rolling hills of the Highveld, painting the Gauteng skies in shades of gold and amber. The dusty streets of Midrand begin to hum with the early morning life, as commuters fill the platforms and local merchants raise their shutters.\n\nIt was here that Khwezi first realized that the stories written in these lands had a rhythm of their own—a heartbeat that echoed the rich histories of those who walked before.",
|
||||
"Chapter 2.\n\nAs the afternoon heat settles over the vast landscapes, a quiet stillness descends. Under the shade of an ancient thorn tree, old journals are opened, revealing handwritten accounts of early migrations, family secrets, and triumphant breakthroughs. The digital DRM safeguards preserve these fragile pages for generations to come."
|
||||
};
|
||||
|
||||
CatalogBooks.Add(book);
|
||||
}
|
||||
|
||||
FeaturedBooks = CatalogBooks.Where(b => b.Bestseller || b.Trending).Take(3).ToList();
|
||||
|
||||
// Pre-purchase one book to show active items in Library immediately
|
||||
PurchasedRecords.Add(new PurchaseRecord
|
||||
{
|
||||
BookId = "book-seed-3",
|
||||
Format = "ebook",
|
||||
CurrentPage = 0,
|
||||
Progress = 15
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleActiveTabChanged(string tab)
|
||||
{
|
||||
ActiveTab = tab;
|
||||
}
|
||||
|
||||
private void HandleOpenCart()
|
||||
{
|
||||
IsCartOpen = true;
|
||||
}
|
||||
|
||||
private void HandleCloseCart()
|
||||
{
|
||||
IsCartOpen = false;
|
||||
}
|
||||
|
||||
private void HandleOfflineToggle(bool value)
|
||||
{
|
||||
IsOfflineMode = value;
|
||||
}
|
||||
|
||||
private void HandleSelectBook(BookModel book)
|
||||
{
|
||||
SelectedBook = book;
|
||||
}
|
||||
|
||||
private void HandleCloseModal()
|
||||
{
|
||||
SelectedBook = null;
|
||||
}
|
||||
|
||||
private void HandleAddToCart((BookModel Book, string Format) tuple)
|
||||
{
|
||||
if (!CartItems.Any(i => i.Book.Id == tuple.Book.Id && i.Format == tuple.Format))
|
||||
{
|
||||
CartItems.Add(new CartItem { Book = tuple.Book, Format = tuple.Format });
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAddToCartFromModal(string format)
|
||||
{
|
||||
if (SelectedBook != null)
|
||||
{
|
||||
HandleAddToCart((SelectedBook, format));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRemoveFromCart(CartItem item)
|
||||
{
|
||||
CartItems.Remove(item);
|
||||
}
|
||||
|
||||
private void HandleCheckoutSuccess(List<CartItem> items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!PurchasedRecords.Any(p => p.BookId == item.Book.Id && p.Format == item.Format))
|
||||
{
|
||||
PurchasedRecords.Add(new PurchaseRecord
|
||||
{
|
||||
BookId = item.Book.Id,
|
||||
Format = item.Format,
|
||||
CurrentPage = 0,
|
||||
Progress = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
CartItems.Clear();
|
||||
ActiveTab = "library";
|
||||
}
|
||||
|
||||
private void HandleReadEbook(BookModel book)
|
||||
{
|
||||
ActiveReadingBook = book;
|
||||
var purchase = PurchasedRecords.FirstOrDefault(p => p.BookId == book.Id && p.Format == "ebook");
|
||||
ActiveReadingPage = purchase?.CurrentPage ?? 0;
|
||||
SelectedBook = null; // Close detail modal if open
|
||||
}
|
||||
|
||||
private void HandleCloseReader()
|
||||
{
|
||||
ActiveReadingBook = null;
|
||||
}
|
||||
|
||||
private void HandlePageProgressChanged(int page)
|
||||
{
|
||||
ActiveReadingPage = page;
|
||||
if (ActiveReadingBook != null)
|
||||
{
|
||||
var purchase = PurchasedRecords.FirstOrDefault(p => p.BookId == ActiveReadingBook.Id && p.Format == "ebook");
|
||||
if (purchase != null)
|
||||
{
|
||||
purchase.CurrentPage = page;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePlayAudiobook(BookModel book)
|
||||
{
|
||||
ActivePlayingBook = book;
|
||||
IsAudioPlaying = true;
|
||||
SelectedBook = null; // Close detail modal if open
|
||||
}
|
||||
|
||||
private void HandleCloseAudioPlayer()
|
||||
{
|
||||
ActivePlayingBook = null;
|
||||
IsAudioPlaying = false;
|
||||
}
|
||||
|
||||
private void HandleAudioPlayToggle(bool isPlaying)
|
||||
{
|
||||
IsAudioPlaying = isPlaying;
|
||||
}
|
||||
|
||||
private void HandleCacheBookOffline(string bookId)
|
||||
{
|
||||
if (!OfflineCachedBooks.Contains(bookId))
|
||||
{
|
||||
OfflineCachedBooks.Add(bookId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAddCategory(string category)
|
||||
{
|
||||
if (!Categories.Contains(category))
|
||||
{
|
||||
Categories.Add(category);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePublishBook(BookModel book)
|
||||
{
|
||||
CatalogBooks.Insert(0, book);
|
||||
}
|
||||
|
||||
private void HandleRateBook(string bookId, int rating)
|
||||
{
|
||||
int? oldRating = UserRatings.ContainsKey(bookId) ? UserRatings[bookId] : null;
|
||||
UserRatings[bookId] = rating;
|
||||
|
||||
var book = CatalogBooks.FirstOrDefault(b => b.Id == bookId);
|
||||
if (book != null)
|
||||
{
|
||||
double oldTotal = book.Rating * book.ReviewsCount;
|
||||
if (oldRating.HasValue)
|
||||
{
|
||||
book.Rating = (oldTotal - oldRating.Value + rating) / Math.Max(1, book.ReviewsCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
book.ReviewsCount += 1;
|
||||
book.Rating = (oldTotal + rating) / book.ReviewsCount;
|
||||
}
|
||||
book.Rating = Math.Round(book.Rating * 10) / 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user