456 lines
27 KiB
Plaintext
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 & 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
|
|
});
|
|
}
|
|
}
|
|
}
|