Files
midrandbooks/MidrandBookshop/Components/Pages/AuthorPortal.razor
T
2026-07-18 12:08:14 +02:00

456 lines
27 KiB
Plaintext

@namespace MidrandBooks.Components.Pages
@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 &amp; 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 &amp; 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 &amp; 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
});
}
}
}