Updated design
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
@namespace MidrandBooks.Components
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using MidrandBooks.Models
|
||||
|
||||
<div id="razor-author-portal" class="max-w-7xl mx-auto px-4 sm:px-6 py-6 space-y-6">
|
||||
<div class="border border-slate-200/60 bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||
|
||||
<!-- Tab Headers -->
|
||||
<div class="flex border-b border-slate-100 bg-slate-50/50">
|
||||
<button
|
||||
@onclick='() => SetActiveTab("publish")'
|
||||
class="px-6 py-4 font-serif text-sm font-bold border-b-2 transition-all cursor-pointer @(ActiveTab == "publish" ? "border-violet-600 text-violet-700 bg-white" : "border-transparent text-slate-500 hover:text-slate-800")">
|
||||
Publish New Manuscript
|
||||
</button>
|
||||
<button
|
||||
@onclick='() => SetActiveTab("catalog")'
|
||||
class="px-6 py-4 font-serif text-sm font-bold border-b-2 transition-all cursor-pointer @(ActiveTab == "catalog" ? "border-violet-600 text-violet-700 bg-white" : "border-transparent text-slate-500 hover:text-slate-800")">
|
||||
My Author Catalog & Moderation Panel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
@if (ActiveTab == "publish")
|
||||
{
|
||||
<!-- Publish Tab Content -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<!-- Left Form Section (8 cols) -->
|
||||
<div class="lg:col-span-7 space-y-5">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-serif text-lg font-bold text-slate-800">Establish Direct SA Distribution</h3>
|
||||
<p class="text-[11px] text-slate-500 leading-relaxed">
|
||||
Upload your manuscripts directly. Your files are converted in the background into copy-protected, DRM-sandboxed reflowable epub formats ready for legal distribution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Title Input -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-mono text-slate-500 font-bold uppercase block">Manuscript Title</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Whispers of the Highveld"
|
||||
@bind="NewTitle"
|
||||
class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15 focus:border-violet-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Category Selector with Custom Creation Options -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-[10px] font-mono text-slate-500 font-bold uppercase block">Genre Category</label>
|
||||
<button
|
||||
type="button"
|
||||
@onclick="ToggleCustomCategory"
|
||||
class="text-[10px] text-violet-600 hover:text-violet-500 font-bold font-mono flex items-center gap-1 cursor-pointer">
|
||||
@(ShowCustomCategoryInput ? "Existing" : "New Genre")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (ShowCustomCategoryInput)
|
||||
{
|
||||
<div class="flex gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Afrofuturism"
|
||||
@bind="CustomCategoryName"
|
||||
class="flex-1 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15 transition-all"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@onclick="SaveCustomCategory"
|
||||
class="px-3 rounded-xl bg-violet-600 hover:bg-violet-500 text-white text-xs font-bold cursor-pointer transition-all">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<select
|
||||
@bind="SelectedCategory"
|
||||
class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15 focus:border-violet-500 transition-all">
|
||||
@foreach (var cat in Categories)
|
||||
{
|
||||
<option value="@cat">@cat</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-mono text-slate-500 font-bold uppercase block">South African Pricing (ZAR)</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-3 flex items-center text-slate-500 font-mono text-xs font-bold">R</span>
|
||||
<input
|
||||
type="number"
|
||||
@bind="NewPrice"
|
||||
class="w-full rounded-xl border border-slate-200 bg-slate-50 pl-7 pr-3 py-2.5 text-xs text-slate-800 font-mono outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15 focus:border-violet-500 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drag and Drop Cover Art Image Upload Area -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-[10px] font-mono text-slate-500 font-bold uppercase block">Custom Cover Artwork Image (Optional)</label>
|
||||
@if (!string.IsNullOrEmpty(CoverImagePreview))
|
||||
{
|
||||
<button
|
||||
type="button"
|
||||
@onclick="ClearCoverImage"
|
||||
class="text-[9px] text-red-500 hover:text-red-400 font-mono cursor-pointer">
|
||||
Clear Image
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="border border-dashed rounded-xl p-3 text-center transition-all cursor-pointer flex items-center justify-center gap-3 @(string.IsNullOrEmpty(CoverImagePreview) ? "border-slate-200 bg-slate-50/50 hover:bg-slate-50 text-slate-500" : "border-violet-300 bg-violet-50/10")"
|
||||
@onclick="TriggerCoverUpload">
|
||||
|
||||
@if (!string.IsNullOrEmpty(CoverImagePreview))
|
||||
{
|
||||
<div class="flex items-center gap-3 w-full text-left">
|
||||
<img src="@CoverImagePreview" alt="Cover preview" class="h-10 w-8 rounded object-cover shadow-sm flex-shrink-0" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<span class="block text-[11px] font-bold text-slate-700 truncate font-mono">@CoverImageName</span>
|
||||
<span class="block text-[9px] text-slate-400">Custom image set as primary book artwork</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex items-center gap-2 text-slate-400 py-1">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
<span class="text-[11px] font-bold text-slate-600">Simulate Drag/Drop or click to upload Cover Art</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brief blurb description -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-mono text-slate-500 font-bold uppercase">Brief Book Description (Blurb)</label>
|
||||
<textarea
|
||||
placeholder="Summarize your story for prospective buyers..."
|
||||
@bind="NewDescription"
|
||||
rows="3"
|
||||
class="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 text-xs text-slate-800 placeholder-slate-400 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15 focus:border-violet-500 transition-all resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@onclick="PublishManuscript"
|
||||
class="w-full py-4 rounded-xl bg-violet-600 hover:bg-violet-500 text-white font-bold text-xs shadow-md cursor-pointer transition-all flex items-center justify-center gap-2">
|
||||
Publish Manuscript for Moderation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right Info / Guidelines Column (5 cols) -->
|
||||
<div class="lg:col-span-5 bg-slate-50 rounded-2xl p-5 border border-slate-100 space-y-4">
|
||||
<h4 class="font-serif text-sm font-bold text-slate-800 flex items-center gap-1.5">
|
||||
⚖ Publishing Laws & Moderation Mandates
|
||||
</h4>
|
||||
|
||||
<div class="space-y-3.5 text-slate-600 text-[11px] leading-relaxed">
|
||||
<p>
|
||||
Midrand Books operates in direct conformity with the **South African National Library Services Act and Copyright protection provisions**. Every submission undergoes rigorous verification:
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2.5">
|
||||
<span class="p-1 rounded bg-violet-100 text-violet-700 h-5 w-5 flex items-center justify-center text-[10px] font-bold">1</span>
|
||||
<div>
|
||||
<strong class="text-slate-800 block">Immediate Background Conversion</strong>
|
||||
Files are dispatched to an asynchronous server node to establish reflowable reading templates, embedded copy-protection, and sandboxed audio DRM loops.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2.5">
|
||||
<span class="p-1 rounded bg-violet-100 text-violet-700 h-5 w-5 flex items-center justify-center text-[10px] font-bold">2</span>
|
||||
<div>
|
||||
<strong class="text-slate-800 block">Editorial Moderation Curation</strong>
|
||||
All books must meet formatting criteria and pass legal compliance checks prior to public listing.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2.5">
|
||||
<span class="p-1 rounded bg-violet-100 text-violet-700 h-5 w-5 flex items-center justify-center text-[10px] font-bold">3</span>
|
||||
<div>
|
||||
<strong class="text-slate-800 block">Real-time Correction Logs</strong>
|
||||
If issues are found, the system records moderation logs and returns feedback so authors can easily submit updated artwork or texts.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (ActiveTab == "catalog")
|
||||
{
|
||||
<!-- Catalog Tab Content with simulator capabilities -->
|
||||
<div class="space-y-6">
|
||||
@if (MyBooks.Any())
|
||||
{
|
||||
@foreach (var book in MyBooks)
|
||||
{
|
||||
<div class="border border-slate-200 bg-slate-50/50 rounded-2xl p-5 flex flex-col md:flex-row gap-5 transition-all hover:bg-white hover:shadow-md relative overflow-hidden">
|
||||
|
||||
<!-- Jacket Mockup -->
|
||||
<div class="h-36 w-24 rounded-xl bg-slate-900 flex-shrink-0 shadow-md relative overflow-hidden border border-slate-200/80 mx-auto md:mx-0">
|
||||
@if (!string.IsNullOrEmpty(book.CoverUrl))
|
||||
{
|
||||
<img src="@book.CoverUrl" alt="@book.Title" class="h-full w-full object-cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="h-full w-full bg-gradient-to-br @book.CoverColor p-2.5 flex flex-col justify-between text-white">
|
||||
<span class="text-[7px] font-mono font-bold tracking-widest uppercase text-white/50">@book.Category</span>
|
||||
<span class="text-[10px] font-serif font-extrabold line-clamp-3 leading-tight">@book.Title</span>
|
||||
<span class="text-[7px] font-sans text-white/85 truncate block">By @book.Author</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Details and simulation buttons -->
|
||||
<div class="flex-1 min-w-0 flex flex-col justify-between space-y-4">
|
||||
<div>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h4 class="font-serif text-base font-bold text-slate-800 leading-snug">@book.Title</h4>
|
||||
<p class="text-[10px] text-slate-500 font-mono mt-0.5">Category: @book.Category • Price: R @book.Price.ToString("F2")</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Moderation Box -->
|
||||
<div class="p-3.5 rounded-xl border bg-white space-y-2 text-xs">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-[10px] font-mono font-bold uppercase text-slate-400">Moderation Status</span>
|
||||
|
||||
@if (book.Status == "published")
|
||||
{
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-100">
|
||||
Live & Active
|
||||
</span>
|
||||
}
|
||||
else if (book.Status == "converting")
|
||||
{
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-amber-50 text-amber-700 border border-amber-100 animate-pulse">
|
||||
Converting PDF...
|
||||
</span>
|
||||
}
|
||||
else if (book.Status == "pending_review")
|
||||
{
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-blue-50 text-blue-700 border border-blue-100">
|
||||
Pending Human Review
|
||||
</span>
|
||||
}
|
||||
else if (book.Status == "rejected")
|
||||
{
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-50 text-rose-700 border border-rose-100">
|
||||
Rejected / Revision Required
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-red-100 text-red-800 border border-red-200">
|
||||
Compliance Take Down
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p class="text-[11px] text-slate-600 leading-relaxed">
|
||||
<strong>Moderator Feedback:</strong> @(book.ModerationFeedback ?? "No feedback provided yet.")
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Live Status Simulations (Allows testing reviewer outcomes instantly) -->
|
||||
<div class="border-t border-slate-100 pt-3 flex flex-wrap items-center justify-between gap-3 bg-violet-50/10 p-3 rounded-xl border border-violet-500/10">
|
||||
<span class="text-[10px] font-bold font-mono text-violet-700">Simulate Curator Actions:</span>
|
||||
<div class="flex gap-1.5">
|
||||
<button
|
||||
@onclick='() => SimulateAction(book.Id, "published", "Curation review passed successfully. Compliant with legal deposit requirements. Published to public index.")'
|
||||
class="px-2.5 py-1 rounded bg-emerald-600 hover:bg-emerald-500 text-white text-[9px] font-bold cursor-pointer transition-colors">
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
@onclick='() => SimulateAction(book.Id, "rejected", "Low resolution cover art detected on jacket preview. Please re-upload high contrast art.")'
|
||||
class="px-2.5 py-1 rounded bg-amber-600 hover:bg-amber-500 text-white text-[9px] font-bold cursor-pointer transition-colors">
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
@onclick='() => SimulateAction(book.Id, "taken_down", "Copyright violation match detected. Digital Millenium Copyright Act claim registered.")'
|
||||
class="px-2.5 py-1 rounded bg-rose-600 hover:bg-rose-500 text-white text-[9px] font-bold cursor-pointer transition-colors">
|
||||
Take Down
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="p-12 text-center border border-dashed border-slate-200 bg-slate-50/50 rounded-3xl space-y-2">
|
||||
<svg class="mx-auto h-8 w-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<h4 class="font-serif text-sm font-bold text-slate-700">No self-published titles registered</h4>
|
||||
<p class="text-[10px] text-slate-500 font-sans max-w-sm mx-auto leading-relaxed">
|
||||
Create and publish your very first manuscript on the adjacent tab to load tracking, logs, and moderation tools!
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<string> Categories { get; set; } = new();
|
||||
[Parameter] public EventCallback<string> OnAddCategory { get; set; }
|
||||
[Parameter] public EventCallback<BookModel> OnPublishBook { get; set; }
|
||||
|
||||
private string ActiveTab { get; set; } = "publish";
|
||||
private string NewTitle { get; set; } = string.Empty;
|
||||
private string SelectedCategory { get; set; } = "Fiction";
|
||||
private string CustomCategoryName { get; set; } = string.Empty;
|
||||
private bool ShowCustomCategoryInput { get; set; } = false;
|
||||
private double NewPrice { get; set; } = 120.00;
|
||||
private string NewDescription { get; set; } = string.Empty;
|
||||
|
||||
// Cover art simulation previews
|
||||
private string CoverImagePreview { get; set; } = string.Empty;
|
||||
private string CoverImageName { get; set; } = string.Empty;
|
||||
|
||||
private List<BookModel> MyBooks { get; set; } = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Categories.Any())
|
||||
{
|
||||
SelectedCategory = Categories.First();
|
||||
}
|
||||
|
||||
// Add pre-loaded simulated book for the user on portal initialization
|
||||
MyBooks.Add(new BookModel
|
||||
{
|
||||
Id = "pre-loaded-1",
|
||||
Title = "Highveld Sunset Stories",
|
||||
Author = "Author Khwezi",
|
||||
AuthorId = "khwezi-auth-1",
|
||||
Price = 145.00,
|
||||
Category = "African Literature",
|
||||
CoverColor = "from-rose-950 to-orange-900",
|
||||
Status = "pending_review",
|
||||
ModerationFeedback = "In queue for legal deposit assessment.",
|
||||
ModerationHistory = new List<ModerationHistoryEntry>
|
||||
{
|
||||
new ModerationHistoryEntry { Status = "converting", Date = "12/07/2026 10:14", Feedback = "Background compilation complete." }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void SetActiveTab(string tab)
|
||||
{
|
||||
ActiveTab = tab;
|
||||
}
|
||||
|
||||
private void ToggleCustomCategory()
|
||||
{
|
||||
ShowCustomCategoryInput = !ShowCustomCategoryInput;
|
||||
}
|
||||
|
||||
private async Task SaveCustomCategory()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(CustomCategoryName))
|
||||
{
|
||||
var cat = CustomCategoryName.Trim();
|
||||
await OnAddCategory.InvokeAsync(cat);
|
||||
SelectedCategory = cat;
|
||||
CustomCategoryName = string.Empty;
|
||||
ShowCustomCategoryInput = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerCoverUpload()
|
||||
{
|
||||
// Simulated cover file selection action
|
||||
CoverImagePreview = "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?q=80&w=600&auto=format&fit=crop";
|
||||
CoverImageName = "sunset-artwork-preview-hires.jpg";
|
||||
}
|
||||
|
||||
private void ClearCoverImage()
|
||||
{
|
||||
CoverImagePreview = string.Empty;
|
||||
CoverImageName = string.Empty;
|
||||
}
|
||||
|
||||
private async Task PublishManuscript()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewTitle)) return;
|
||||
|
||||
var book = new BookModel
|
||||
{
|
||||
Id = "self-pub-" + Guid.NewGuid().ToString().Substring(0, 8),
|
||||
Title = NewTitle,
|
||||
Author = "Author Khwezi",
|
||||
Price = NewPrice,
|
||||
Category = SelectedCategory,
|
||||
CoverUrl = CoverImagePreview,
|
||||
CoverColor = "from-indigo-900 to-purple-950",
|
||||
Description = NewDescription,
|
||||
Status = "converting",
|
||||
ModerationFeedback = "Background ingestion process active. Converting raw PDF structure to secure reflowable ePub nodes...",
|
||||
ModerationHistory = new List<ModerationHistoryEntry>
|
||||
{
|
||||
new ModerationHistoryEntry
|
||||
{
|
||||
Status = "converting",
|
||||
Date = DateTime.Now.ToString("dd/MM/yyyy HH:mm"),
|
||||
Feedback = "Manuscript uploaded. Worker assigned."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MyBooks.Add(book);
|
||||
await OnPublishBook.InvokeAsync(book);
|
||||
|
||||
// Reset inputs
|
||||
NewTitle = string.Empty;
|
||||
NewDescription = string.Empty;
|
||||
CoverImagePreview = string.Empty;
|
||||
CoverImageName = string.Empty;
|
||||
ActiveTab = "catalog";
|
||||
}
|
||||
|
||||
private void SimulateAction(string bookId, string targetStatus, string feedback)
|
||||
{
|
||||
var book = MyBooks.FirstOrDefault(b => b.Id == bookId);
|
||||
if (book != null)
|
||||
{
|
||||
book.Status = targetStatus;
|
||||
book.ModerationFeedback = feedback;
|
||||
book.ModerationHistory.Insert(0, new ModerationHistoryEntry
|
||||
{
|
||||
Status = targetStatus,
|
||||
Date = DateTime.Now.ToString("dd/MM/yyyy HH:mm"),
|
||||
Feedback = feedback
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
@namespace MidrandBooks.Components
|
||||
@using Microsoft.AspNetCore.Components
|
||||
|
||||
<div id="error-razor" class="w-full max-w-xl mx-auto px-4 py-16 text-center space-y-6">
|
||||
<div class="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-red-500/10 text-red-600 border border-red-500/20">
|
||||
<svg class="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h2 class="font-serif text-2xl md:text-3xl font-bold text-slate-800">Something went wrong</h2>
|
||||
<p class="text-xs text-slate-500 max-w-sm mx-auto leading-relaxed">
|
||||
@Message
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ErrorCode))
|
||||
{
|
||||
<div class="p-3 bg-slate-50 rounded-xl border border-slate-100 inline-block font-mono text-[10px] text-slate-400">
|
||||
Error Code: <span class="font-bold text-slate-600">@ErrorCode</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex justify-center gap-3 pt-2">
|
||||
@if (OnRetry.HasDelegate)
|
||||
{
|
||||
<button @onclick="HandleRetry" class="px-5 py-2 rounded-xl bg-violet-600 hover:bg-violet-500 text-white font-bold text-xs transition-all shadow-md shadow-violet-500/15 cursor-pointer">
|
||||
Try Again
|
||||
</button>
|
||||
}
|
||||
<button @onclick="HandleBackToHome" class="px-5 py-2 rounded-xl border border-slate-200 hover:bg-slate-50 text-slate-600 font-bold text-xs transition-all cursor-pointer">
|
||||
Back to Catalog
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Message { get; set; } = "An unexpected error occurred while processing your request. Please try again later.";
|
||||
[Parameter] public string? ErrorCode { get; set; } = "ERR_CONNECTION_FAILED";
|
||||
[Parameter] public EventCallback OnRetry { get; set; }
|
||||
[Parameter] public EventCallback OnBackToHome { get; set; }
|
||||
|
||||
private async Task HandleRetry()
|
||||
{
|
||||
await OnRetry.InvokeAsync();
|
||||
}
|
||||
|
||||
private async Task HandleBackToHome()
|
||||
{
|
||||
await OnBackToHome.InvokeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@page "/"
|
||||
@namespace MidrandBooks.Components.Pages
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Active MainLayout triggers rendering and manages central state. This Home page acts as the default routing node in Blazor structures. -->
|
||||
<div class="hidden">
|
||||
<span class="font-mono text-[9px] text-slate-400">Blazor route target loaded.</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,223 @@
|
||||
@namespace MidrandBooks.Components
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using MidrandBooks.Models
|
||||
|
||||
<div id="library-view-razor" class="w-full max-w-7xl mx-auto px-4 sm:px-6 my-10 space-y-8 min-h-[500px]">
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-end justify-between gap-4 border-b border-slate-100 pb-4">
|
||||
<div>
|
||||
<h2 class="font-serif text-2xl md:text-3xl font-bold text-slate-800 flex items-center gap-2">
|
||||
<svg class="h-7 w-7 text-slate-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
My Secure Library
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Access your licensed digital purchases. Sync progress instantly across all browsers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Format Filter Tabs -->
|
||||
<div class="flex bg-slate-100 border border-slate-200/60 p-1 rounded-xl self-start sm:self-auto">
|
||||
@foreach (var format in Formats)
|
||||
{
|
||||
<button
|
||||
@onclick="() => FilterByFormat(format)"
|
||||
class="px-4 py-1.5 rounded-lg text-xs font-semibold uppercase tracking-wider transition-all cursor-pointer @(ActiveFormat == format ? "bg-white text-slate-900 shadow-sm border border-slate-200/50" : "text-slate-500 hover:text-slate-800")">
|
||||
@format
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid Layout -->
|
||||
@if (FilteredLibraryItems().Any())
|
||||
{
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@foreach (var item in FilteredLibraryItems())
|
||||
{
|
||||
var book = item.Book;
|
||||
var isEbook = item.Format == "ebook";
|
||||
var isCached = OfflineBooks.Contains(book.Id);
|
||||
var isDownloading = DownloadingBookId == book.Id;
|
||||
|
||||
<div class="group relative flex gap-5 rounded-2xl p-4 border border-slate-100 bg-white/70 backdrop-blur-md shadow-sm hover:shadow-md transition-all">
|
||||
|
||||
<!-- Book Cover -->
|
||||
<div @onclick="() => HandleSelectBook(book)" class="relative h-36 w-24 rounded-xl overflow-hidden shadow-sm bg-slate-100 flex-shrink-0 cursor-pointer">
|
||||
@if (!string.IsNullOrEmpty(book.CoverUrl))
|
||||
{
|
||||
<img src="@book.CoverUrl" alt="@book.Title" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="h-full w-full bg-gradient-to-br @book.CoverColor p-3 flex flex-col justify-between text-white transition-transform duration-500 group-hover:scale-105">
|
||||
<span class="text-[6px] font-mono font-bold tracking-widest block uppercase text-white/40">@book.Category</span>
|
||||
<span class="text-xs font-serif font-extrabold line-clamp-3 leading-tight text-white">@book.Title</span>
|
||||
<span class="text-[8px] font-sans text-white/70 truncate block">By @book.Author</span>
|
||||
</div>
|
||||
}
|
||||
<div class="absolute top-0 bottom-0 left-0 w-2 bg-gradient-to-r from-black/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
<!-- Details Node -->
|
||||
<div class="flex-1 min-w-0 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="inline-block text-[8px] font-mono font-extrabold uppercase rounded px-1.5 py-0.5 @(isEbook ? "bg-indigo-50 text-indigo-700" : "bg-violet-50 text-violet-700")">
|
||||
@item.Format
|
||||
</span>
|
||||
|
||||
<!-- Offline Cache Trigger Button -->
|
||||
@if (isEbook)
|
||||
{
|
||||
@if (isCached)
|
||||
{
|
||||
<span class="inline-flex items-center gap-0.5 text-[9px] font-semibold text-emerald-600 font-mono" title="Synced Offline Cached copy secure">
|
||||
✓ Cached
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button @onclick="() => TriggerCacheBook(book.Id)" disabled="@isDownloading" class="text-[9px] font-bold text-violet-600 hover:text-violet-800 flex items-center gap-0.5 cursor-pointer disabled:opacity-50">
|
||||
@if (isDownloading)
|
||||
{
|
||||
<span>Caching...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>↓ Sync Offline</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 @onclick="() => HandleSelectBook(book)" class="font-serif text-sm font-bold text-slate-800 leading-tight mt-1.5 hover:text-violet-600 cursor-pointer truncate">
|
||||
@book.Title
|
||||
</h3>
|
||||
<span class="text-[10px] text-slate-400 block mt-0.5">By @book.Author</span>
|
||||
|
||||
<!-- Progress Track Slider bar -->
|
||||
<div class="mt-3.5 space-y-1">
|
||||
<div class="flex justify-between text-[8px] font-mono font-bold text-slate-400">
|
||||
<span>READING PROGRESS</span>
|
||||
<span>@(isEbook ? $"Page {item.CurrentPage + 1}" : "In Progress")</span>
|
||||
</div>
|
||||
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div class="bg-violet-600 h-full rounded-full transition-all" style="width: @(isEbook ? $"{item.Progress}%" : "15%")" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access Action Buttons -->
|
||||
<div class="mt-4 flex gap-2">
|
||||
@if (isEbook)
|
||||
{
|
||||
<button @onclick="() => HandleReadBook(book)" class="flex-1 py-1.5 text-center text-[10px] font-bold rounded-xl bg-slate-900 hover:bg-slate-800 text-white shadow-sm transition-colors cursor-pointer flex items-center justify-center gap-1">
|
||||
Open Ebook
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button @onclick="() => HandlePlayBook(book)" class="flex-1 py-1.5 text-center text-[10px] font-bold rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 shadow-sm transition-colors cursor-pointer flex items-center justify-center gap-1">
|
||||
Listen Audio
|
||||
</button>
|
||||
}
|
||||
<button @onclick="() => HandleSelectBook(book)" class="p-1.5 rounded-xl border border-slate-200 hover:bg-slate-50 text-slate-500 hover:text-slate-700 cursor-pointer" title="View details">
|
||||
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="p-12 text-center border border-dashed border-slate-200 bg-slate-50/50 rounded-3xl space-y-2">
|
||||
<svg class="mx-auto h-8 w-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
<h4 class="font-serif text-sm font-bold text-slate-700">Your secure library is empty</h4>
|
||||
<p class="text-[10px] text-slate-500 font-sans max-w-sm mx-auto leading-relaxed">
|
||||
Unlock local publishing titles, bestsellers, and studio audiobooks in our browse store. Purchases are instantly activated here.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<PurchaseRecord> Purchases { get; set; } = new();
|
||||
[Parameter] public List<BookModel> Books { get; set; } = new();
|
||||
[Parameter] public List<string> OfflineBooks { get; set; } = new();
|
||||
[Parameter] public bool IsOfflineMode { get; set; }
|
||||
|
||||
[Parameter] public EventCallback<BookModel> OnSelectBook { get; set; }
|
||||
[Parameter] public EventCallback<BookModel> OnReadBook { get; set; }
|
||||
[Parameter] public EventCallback<BookModel> OnPlayBook { get; set; }
|
||||
[Parameter] public EventCallback<string> OnCacheBookOffline { get; set; }
|
||||
|
||||
private string ActiveFormat { get; set; } = "all";
|
||||
private string[] Formats = new string[] { "all", "ebook", "audiobook" };
|
||||
private string? DownloadingBookId { get; set; }
|
||||
|
||||
private void FilterByFormat(string format)
|
||||
{
|
||||
ActiveFormat = format;
|
||||
}
|
||||
|
||||
private IEnumerable<LibraryItem> FilteredLibraryItems()
|
||||
{
|
||||
var items = new List<LibraryItem>();
|
||||
foreach (var purchase in Purchases)
|
||||
{
|
||||
var matchedBook = Books.FirstOrDefault(b => b.Id == purchase.BookId);
|
||||
if (matchedBook != null)
|
||||
{
|
||||
items.Add(new LibraryItem
|
||||
{
|
||||
Book = matchedBook,
|
||||
Format = purchase.Format,
|
||||
CurrentPage = purchase.CurrentPage,
|
||||
Progress = purchase.Progress
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ActiveFormat == "all") return items;
|
||||
return items.Where(i => i.Format.Equals(ActiveFormat, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task TriggerCacheBook(string bookId)
|
||||
{
|
||||
if (DownloadingBookId != null) return;
|
||||
DownloadingBookId = bookId;
|
||||
|
||||
await Task.Delay(1500); // Simulate network latency
|
||||
await OnCacheBookOffline.InvokeAsync(bookId);
|
||||
|
||||
DownloadingBookId = null;
|
||||
}
|
||||
|
||||
private void HandleSelectBook(BookModel book)
|
||||
{
|
||||
OnSelectBook.InvokeAsync(book);
|
||||
}
|
||||
|
||||
private void HandleReadBook(BookModel book)
|
||||
{
|
||||
OnReadBook.InvokeAsync(book);
|
||||
}
|
||||
|
||||
private void HandlePlayBook(BookModel book)
|
||||
{
|
||||
OnPlayBook.InvokeAsync(book);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@namespace MidrandBooks.Components
|
||||
@using Microsoft.AspNetCore.Components
|
||||
|
||||
<div id="notfound-razor" class="w-full max-w-xl mx-auto px-4 py-16 text-center space-y-6">
|
||||
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-slate-100 text-slate-400 border border-slate-200/60">
|
||||
<svg class="h-10 w-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h2 class="font-serif text-3xl font-extrabold text-slate-800 tracking-tight">404</h2>
|
||||
<h3 class="font-serif text-lg font-bold text-slate-700">Page Not Found</h3>
|
||||
<p class="text-xs text-slate-500 max-w-sm mx-auto leading-relaxed">
|
||||
The literary masterpiece or digital section you are trying to find doesn't exist or has been relocated to another shelf.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-3 pt-2">
|
||||
<button @onclick="HandleBackToHome" class="px-5 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs transition-all shadow-md cursor-pointer">
|
||||
Return to Storefront
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public EventCallback OnBackToHome { get; set; }
|
||||
|
||||
private async Task HandleBackToHome()
|
||||
{
|
||||
await OnBackToHome.InvokeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
@page "/privacy"
|
||||
@namespace MidrandBooks.Components
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div id="privacy-razor" class="w-full max-w-7xl mx-auto px-4 sm:px-6 my-10 space-y-8 min-h-[600px] animate-in fade-in duration-300">
|
||||
|
||||
<!-- Header Hero Banner -->
|
||||
<header class="relative overflow-hidden rounded-3xl border border-violet-500/30 bg-slate-900 p-8 md:p-12 shadow-xl flex flex-col justify-between gap-6">
|
||||
<div class="absolute top-0 right-0 h-64 w-64 bg-violet-600/10 rounded-full blur-3xl pointer-events-none" />
|
||||
<div class="absolute bottom-0 left-0 h-40 w-40 bg-indigo-600/10 rounded-full blur-2xl pointer-events-none" />
|
||||
|
||||
<div class="space-y-4 max-w-3xl relative z-10">
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-violet-500/20 border border-violet-500/30 text-xs font-mono font-bold text-violet-300">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
Regulatory Governance
|
||||
</div>
|
||||
<h1 class="font-serif text-3xl md:text-4xl font-black text-white tracking-tight">
|
||||
Privacy & Data Protection Policy
|
||||
</h1>
|
||||
<p class="text-xs md:text-sm text-slate-300 leading-relaxed font-medium font-sans">
|
||||
Midrand Books is designed, maintained, owned, and hosted by <strong class="text-violet-300">LiteCharms (PTY) Ltd</strong> (<a href="https://www.litecharms.co.za" target="_blank" rel="noopener noreferrer" class="text-violet-400 hover:text-violet-300 underline">www.litecharms.co.za</a>), a company duly incorporated and registered in accordance with the Companies Act of South Africa (Registration No: <span class="font-mono text-xs">2020/269438/07</span>).
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||
|
||||
<!-- Sidebar Navigation Index -->
|
||||
<div class="lg:col-span-3 lg:sticky lg:top-24 hidden lg:block">
|
||||
<div class="rounded-2xl border border-white/10 bg-slate-900/60 backdrop-blur-md p-5 space-y-4 shadow-sm">
|
||||
<span class="text-[10px] font-mono font-bold tracking-wider text-slate-400 uppercase block pb-2 border-b border-white/5">
|
||||
Policy Sections
|
||||
</span>
|
||||
<nav class="flex flex-col gap-2 font-mono text-[11px] font-bold text-slate-300 uppercase">
|
||||
<button @onclick='() => ScrollToSection("section-processing")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
1. Lawful Processing
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-collection")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
2. Information We Collect
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-identity")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
3. Identity Nodes
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-payment")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
4. PayFast Gateway
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-rights")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
5. Data Subject Rights
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Legal Article Body -->
|
||||
<div class="lg:col-span-9 space-y-6">
|
||||
<div class="rounded-2xl border border-white/10 bg-slate-900/40 backdrop-blur-md p-6 sm:p-8 shadow-sm space-y-8 text-slate-200 text-xs sm:text-sm leading-relaxed">
|
||||
|
||||
<section id="section-processing" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
1. Commitment to POPIA Compliance
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
LiteCharms (PTY) Ltd trading as Midrand Books ("we", "us", "our") is dedicated to protecting the privacy and personal data of our customers in strict alignment with the <strong>Protection of Personal Information Act, No. 4 of 2013 (POPIA)</strong> of South Africa. We act as the "Responsible Party" for all data collected across our web architecture.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-collection" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
2. Collection of Personal Information
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
We collect personal information solely to facilitate the cataloging, transaction, and dispatch of archival material. This information includes, but is not limited to:
|
||||
</p>
|
||||
<ul class="list-disc pl-5 space-y-2 text-slate-300 font-medium">
|
||||
<li><strong class="text-white">Identity Details:</strong> Name, email address, and verification markers via federated claim contexts.</li>
|
||||
<li><strong class="text-white">Logistics Metrics:</strong> Physical shipping addresses and localized postal codes for courier assignment within South African borders.</li>
|
||||
<li><strong class="text-white">Digital Footprints:</strong> IP addresses, access telemetry, and structural tracking metrics.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="section-identity" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
3. Decentralized Claims & Identity Security
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
For enhanced systemic protection, your core security credentials, access authentication methods, and identity validation vectors are managed externally through our secure cross-tenant single-sign-on architecture (<strong class="text-white">STS Security Cluster</strong>). LiteCharms (PTY) Ltd does not store or process raw account passwords locally on its primary catalog databases.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-payment" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
4. Secure Financial Handover Disclosures
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
All credit card, Instant EFT, and electronic payments are handled through a direct, secure integration handshake with the <strong class="text-white">PayFast (Pty) Ltd</strong> merchant payment system. Midrand Books never captures, indexes, or retains sensitive primary account numbers (PAN), CVV digits, or banking pins. Payment states are transmitted securely using cryptographically signed verification payloads.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-rights" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
5. Data Subject Rights & Information Officer
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
In accordance with POPIA parameters, you reserve explicit rights to request access to, correction of, or total destruction of your personal information stored inside our archives. For any data protection inquiries, or to contact our designated Information Officer, please direct formal correspondence to <code class="bg-slate-950/60 px-2 py-1 rounded text-violet-300 font-bold border border-white/5">privacy@midrandbooks.co.za</code>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private async Task ScrollToSection(string elementId)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("eval", $@"
|
||||
var el = document.getElementById('{elementId}');
|
||||
if (el) {{
|
||||
el.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
|
||||
}}
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
@page "/terms"
|
||||
@namespace MidrandBooks.Components
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div id="terms-razor" class="w-full max-w-7xl mx-auto px-4 sm:px-6 my-10 space-y-8 min-h-[600px] animate-in fade-in duration-300">
|
||||
|
||||
<!-- Header Hero Banner -->
|
||||
<header class="relative overflow-hidden rounded-3xl border border-violet-500/30 bg-slate-900 p-8 md:p-12 shadow-xl flex flex-col justify-between gap-6">
|
||||
<div class="absolute top-0 right-0 h-64 w-64 bg-violet-600/10 rounded-full blur-3xl pointer-events-none" />
|
||||
<div class="absolute bottom-0 left-0 h-40 w-40 bg-indigo-600/10 rounded-full blur-2xl pointer-events-none" />
|
||||
|
||||
<div class="space-y-4 max-w-3xl relative z-10">
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-violet-500/20 border border-violet-500/30 text-xs font-mono font-bold text-violet-300">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
Commercial Legal Framework
|
||||
</div>
|
||||
<h1 class="font-serif text-3xl md:text-4xl font-black text-white tracking-tight">
|
||||
Terms of Sale & Service
|
||||
</h1>
|
||||
<p class="text-xs md:text-sm text-slate-300 leading-relaxed font-medium font-sans">
|
||||
Midrand Books is designed, maintained, owned, and hosted by <strong class="text-violet-300">LiteCharms (PTY) Ltd</strong> (<a href="https://www.litecharms.co.za" target="_blank" rel="noopener noreferrer" class="text-violet-400 hover:text-violet-300 underline">www.litecharms.co.za</a>), a private corporation duly registered and governed in accordance with the corporate statutes of the Companies Act of the Republic of South Africa (Registration No: <span class="font-mono text-xs">2020/269438/07</span>).
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
|
||||
|
||||
<!-- Sidebar Navigation Index -->
|
||||
<div class="lg:col-span-3 lg:sticky lg:top-24 hidden lg:block">
|
||||
<div class="rounded-2xl border border-white/10 bg-slate-900/60 backdrop-blur-md p-5 space-y-4 shadow-sm">
|
||||
<span class="text-[10px] font-mono font-bold tracking-wider text-slate-400 uppercase block pb-2 border-b border-white/5">
|
||||
Agreement Index
|
||||
</span>
|
||||
<nav class="flex flex-col gap-2 font-mono text-[11px] font-bold text-slate-300 uppercase">
|
||||
<button @onclick='() => ScrollToSection("section-formation")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
1. Contract Formation
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-pricing")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
2. Pricing & VAT
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-logistics")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
3. Delivery & Risk
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-cooling")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
4. CPA Cooling-Off
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-liability")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4 text-rose-300 hover:text-rose-200">
|
||||
5. Liability Shield
|
||||
</button>
|
||||
<button @onclick='() => ScrollToSection("section-jurisdiction")' class="px-3 py-2.5 rounded-xl hover:bg-white/5 text-left transition-all hover:text-white cursor-pointer hover:pl-4">
|
||||
6. Jurisdiction
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable Legal Article Body -->
|
||||
<div class="lg:col-span-9 space-y-6">
|
||||
<div class="rounded-2xl border border-white/10 bg-slate-900/40 backdrop-blur-md p-6 sm:p-8 shadow-sm space-y-8 text-slate-200 text-xs sm:text-sm leading-relaxed">
|
||||
|
||||
<section id="section-formation" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
1. Electronic Contract Formation
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
By placing an order on this digital repository layout, you accept these Terms and Conditions in full. In accordance with Section 11 of the <strong class="text-white">Electronic Communications and Transactions Act, No. 25 of 2002 (ECTA)</strong>, these terms are legally binding and enforceable from the second you submit an item request. An order request is only confirmed as a final contract of sale once funds are cleared and the physical inventory has been packed for transit by LiteCharms (PTY) Ltd.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-pricing" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
2. Currency, Value Added Tax, & Inventory Realism
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
All book pricing listed on our site is formatted exclusively in <strong class="text-white">South African Rand (ZAR)</strong>. While we endeavor to ensure immaculate catalog records, typographical pricing errors may occasionally manifest due to currency valuation updates. In such rare anomalies, we explicitly retain the legal prerogative to cancel any affected items prior to dispatch and refund your transaction balance.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-logistics" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
3. Logistics Dispatch & Risk Pass-Through
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
Delivery fulfillment is handled via trusted independent third-party domestic courier providers. Ownership and operational risk concerning physical destruction, item degradation, or total package loss transit pass directly to the consumer at the immediate instant the shipment container leaves our local loading bay docks.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-cooling" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
4. Statutory Returns & CPA Cooling-Off Windows
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
Pursuant to Section 44 of ECTA and the directives of the <strong class="text-white">Consumer Protection Act, No. 68 of 2008 (CPA)</strong>, you maintain a statutory cooling-off window of <strong class="text-white">seven (7) days</strong> following delivery receipt to cancel your transaction without justification, provided the items remain perfectly pristine, unread, and factory-sealed. Return logistics freight costs under a simple cooling-off exercise remain entirely the accountability of the purchaser.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-liability" class="space-y-3 scroll-mt-28 p-5 rounded-2xl bg-rose-500/10 border border-rose-500/20">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-rose-300 flex items-center gap-2">
|
||||
<svg class="h-5.5 w-5.5 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
5. Complete Limitation of Liability Shield
|
||||
</h3>
|
||||
<p class="font-mono text-[10px] font-bold text-rose-400 uppercase tracking-wider block">
|
||||
CRITICAL LEGAL CLAUSE — PLEASE READ CAREFULLY:
|
||||
</p>
|
||||
<p class="font-semibold text-rose-100">
|
||||
To the maximum limit permitted by Section 61 of the Consumer Protection Act, LiteCharms (PTY) Ltd, its underlying software system developers, and structural directors maintain zero liability for any direct, collateral, accidental, or punitive damages resulting from server connectivity breaks, digital database interruptions, payment handoff errors, or systemic down-times. We offer our catalog system entirely on an "As-Is" and "As-Available" functional framework.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="section-jurisdiction" class="space-y-3 scroll-mt-28">
|
||||
<h3 class="font-serif text-lg sm:text-xl font-bold text-white border-b border-white/5 pb-2">
|
||||
6. Governing Law & Jurisdiction
|
||||
</h3>
|
||||
<p class="font-medium text-slate-300">
|
||||
This commercial agreement, catalog usage framework, and transaction architecture are subject strictly to the laws and legal structures of the <strong class="text-white">Republic of South Africa</strong>. Any disputes arising directly out of these digital terms shall be filed and litigated exclusively under the jurisdiction of the High Court of South Africa.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private async Task ScrollToSection(string elementId)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("eval", $@"
|
||||
var el = document.getElementById('{elementId}');
|
||||
if (el) {{
|
||||
el.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
|
||||
}}
|
||||
");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user